From 351403792542753275895746e8d775d6399850f0 Mon Sep 17 00:00:00 2001
From: Wagyourtail <wagyourtail@wagyourtail.xyz>
Date: Mon, 17 Jun 2024 23:59:00 -0500
Subject: [PATCH] Rework logger, Warn on missing stubs

---
 build.gradle.kts                              |   1 +
 .../gradle/coverage/CoverageRunTask.kt        |  56 ++++
 .../gradle/ctsym/GenerateCtSymTask.kt         |  11 +-
 gradle-plugin/build.gradle.kts                |   2 +-
 java-api/build.gradle.kts                     |  48 +--
 .../jvmdg/coverage/ApiCoverageChecker.java    |  16 +-
 .../jvmdg/providers/Java8Downgrader.java      |  27 +-
 site/build.gradle.kts                         |   2 +-
 .../xyz/wagyourtail/jvmdg/site/html/About.kt  |  35 ++-
 .../jvmdg/site/maven/MavenClient.kt           |   7 +-
 .../jvmdg/site/maven/html/Maven.kt            |  21 +-
 .../wagyourtail/jvmdg/ClassDowngrader.java    |  33 +-
 .../classloader/DowngradingClassLoader.java   |  15 +-
 .../xyz/wagyourtail/jvmdg/logging/Logger.java | 179 +++++++++++
 .../wagyourtail/jvmdg/version/Coverage.java   |  93 ++++++
 .../jvmdg/version/VersionProvider.java        | 286 ++++++++++--------
 .../jvmdg/version/all/stub/J_L_Class.java     |   8 +-
 .../jvmdg/version/map/ClassMapping.java       |  45 ++-
 .../xyz/wagyourtail/jvmdg/util/Utils.java     |   6 +
 .../jvmdg/internal/JvmDowngraderTest.java     |   2 +-
 20 files changed, 664 insertions(+), 229 deletions(-)
 create mode 100644 buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/coverage/CoverageRunTask.kt
 create mode 100644 src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java
 create mode 100644 src/main/java/xyz/wagyourtail/jvmdg/version/Coverage.java

diff --git a/build.gradle.kts b/build.gradle.kts
index 5dc5e9c8..f8ec05e0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,7 @@
 import xyz.wagyourtail.gradle.shadow.ShadowJar
 
 plugins {
+    kotlin("jvm") version "1.9.22" apply false
     java
     `maven-publish`
     `java-library`
diff --git a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/coverage/CoverageRunTask.kt b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/coverage/CoverageRunTask.kt
new file mode 100644
index 00000000..a6813980
--- /dev/null
+++ b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/coverage/CoverageRunTask.kt
@@ -0,0 +1,56 @@
+package xyz.wagyourtail.gradle.coverage
+
+import org.gradle.api.JavaVersion
+import org.gradle.api.file.FileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.internal.ConventionTask
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.*
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.jvm.toolchain.JavaToolchainService
+
+abstract class CoverageRunTask : ConventionTask() {
+
+    @get:InputFile
+    abstract val apiJar: RegularFileProperty
+
+    @get:Internal
+    abstract var classpath: FileCollection
+
+    @get:InputFile
+    abstract val ctSym: RegularFileProperty
+
+    @get:Input
+    abstract val javaVersion: Property<JavaVersion>
+
+    @get:OutputDirectory
+    @get:Optional
+    abstract var coverageReports: FileCollection
+
+
+    init {
+        group = "jvmdg"
+        coverageReports = project.files(temporaryDir.resolve("coverage"))
+    }
+
+    @TaskAction
+    fun run() {
+        val toolchains = project.extensions.getByType(JavaToolchainService::class.java)
+
+        project.javaexec { spec ->
+            spec.executable = toolchains.launcherFor {
+                it.languageVersion.set(JavaLanguageVersion.of(javaVersion.get().majorVersion))
+            }.get().executablePath.asFile.absolutePath
+
+            spec.workingDir = temporaryDir
+            spec.mainClass.set("xyz.wagyourtail.jvmdg.coverage.ApiCoverageChecker")
+            spec.classpath = classpath
+            spec.jvmArgs("-Djvmdg.java-api=${apiJar.get().asFile.absolutePath}", "-Djvmdg.quiet=true")
+            spec.args(ctSym.get().asFile.absolutePath)
+
+        }.assertNormalExitValue().rethrowFailure()
+
+    }
+
+
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/ctsym/GenerateCtSymTask.kt b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/ctsym/GenerateCtSymTask.kt
index d073a421..c25da567 100644
--- a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/ctsym/GenerateCtSymTask.kt
+++ b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/ctsym/GenerateCtSymTask.kt
@@ -3,6 +3,7 @@ package xyz.wagyourtail.gradle.ctsym
 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
 import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
 import org.gradle.api.JavaVersion
+import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.internal.ConventionTask
 import org.gradle.api.provider.Property
 import org.gradle.api.tasks.Input
@@ -22,9 +23,9 @@ import kotlin.io.path.*
 
 abstract class GenerateCtSymTask : ConventionTask() {
 
-    @Optional
-    @OutputFile
-    var ctSym = temporaryDir.resolve("jvmdg").resolve("ct.sym")
+    @get:Optional
+    @get:OutputFile
+    abstract val ctSym: RegularFileProperty
 
     @get:Input
     @get:Optional
@@ -34,7 +35,7 @@ abstract class GenerateCtSymTask : ConventionTask() {
     abstract val upperVersion: Property<JavaVersion>
 
     init {
-        outputs.upToDateWhen { ctSym.exists() }
+        ctSym.set(temporaryDir.resolve("jvmdg").resolve("ct.sym"))
         lowerVersion.convention(JavaVersion.VERSION_1_6).finalizeValueOnRead()
         upperVersion.convention(JavaVersion.VERSION_22).finalizeValueOnRead()
     }
@@ -72,7 +73,7 @@ abstract class GenerateCtSymTask : ConventionTask() {
 
 
         ZipArchiveOutputStream(
-            ctSym.toPath().outputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
+            ctSym.get().asFile.toPath().outputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
         ).use { zos ->
             val prevJava = mutableMapOf<String, ClassInfo>()
             for (java in (lowerVersion.get()..upperVersion.get()).reversed()) {
diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts
index 62377003..2f7675de 100644
--- a/gradle-plugin/build.gradle.kts
+++ b/gradle-plugin/build.gradle.kts
@@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 import java.util.*
 
 plugins {
-    kotlin("jvm") version "1.9.22"
+    kotlin("jvm")
     `java-gradle-plugin`
     `java-library`
     `maven-publish`
diff --git a/java-api/build.gradle.kts b/java-api/build.gradle.kts
index 18347102..c406d21d 100644
--- a/java-api/build.gradle.kts
+++ b/java-api/build.gradle.kts
@@ -1,6 +1,7 @@
 import org.objectweb.asm.ClassReader
 import org.objectweb.asm.ClassWriter
 import org.objectweb.asm.tree.ClassNode
+import xyz.wagyourtail.gradle.coverage.CoverageRunTask
 import xyz.wagyourtail.gradle.ctsym.GenerateCtSymTask
 import xyz.wagyourtail.gradle.shadow.ShadowJar
 import xyz.wagyourtail.jvmdg.util.plus
@@ -131,9 +132,38 @@ tasks.getByName<JavaCompile>("compileCoverageJava") {
     configCompile(testVersion)
 }
 
+val coverageApiJar by tasks.registering(Jar::class) {
+    from(sourceSets.main.get().output)
+    from(*((fromVersion..toVersion).map { sourceSets["java${it.ordinal + 1}"].output }).toTypedArray())
+    from(rootProject.sourceSets.getByName("shared").output)
+
+    destinationDirectory = temporaryDir
+}
+
+
+val genCtSym by tasks.registering(GenerateCtSymTask::class) {
+    group = "jvmdg"
+    upperVersion = toVersion - 1
+}
+
+val coverageReport by tasks.registering(CoverageRunTask::class) {
+    group = "jvmdg"
+    dependsOn(coverageApiJar, genCtSym)
+    apiJar.set(coverageApiJar.get().archiveFile.get().asFile)
+    classpath = coverage.runtimeClasspath
+    ctSym.set(genCtSym.get().ctSym)
+    javaVersion.set(testVersion)
+}
+
 tasks.jar {
+    dependsOn(coverageReport)
     from(*((fromVersion..toVersion).map { sourceSets["java${it.ordinal + 1}"].output }).toTypedArray())
     from(rootProject.sourceSets.getByName("shared").output)
+    from(coverageReport.get().coverageReports) {
+        into("META-INF/coverage")
+        exclude("**/unmatched.txt")
+        exclude("**/parentOnly.txt")
+    }
     from(projectDir.parentFile.resolve("LICENSE.md"))
     from(projectDir.parentFile.resolve("license")) {
         into("license")
@@ -253,24 +283,6 @@ val downgradeJar8 by tasks.registering(Jar::class) {
     from(zipTree(tempFile8))
 }
 
-val genCySym by tasks.registering(GenerateCtSymTask::class) {
-    group = "jvmdg"
-    upperVersion = toVersion - 1
-}
-
-val coverageReport by tasks.registering(JavaExec::class) {
-    group = "jvmdg"
-    dependsOn(tasks.jar, genCySym)
-    javaLauncher.set(javaToolchains.launcherFor {
-        languageVersion.set(JavaLanguageVersion.of(testVersion.majorVersion))
-    })
-    mainClass = "xyz.wagyourtail.jvmdg.coverage.ApiCoverageChecker"
-    classpath = coverage.runtimeClasspath
-    jvmArgs("-Djvmdg.java-api=${tasks.jar.get().archiveFile.get().asFile.absolutePath}", "-Djvmdg.quiet=true")
-    args(genCySym.get().ctSym)
-    workingDir = project.layout.buildDirectory.get().asFile
-}
-
 tasks.assemble {
     dependsOn(downgradeJar11)
     dependsOn(downgradeJar8)
diff --git a/java-api/src/coverage/java/xyz/wagyourtail/jvmdg/coverage/ApiCoverageChecker.java b/java-api/src/coverage/java/xyz/wagyourtail/jvmdg/coverage/ApiCoverageChecker.java
index 3ac32bda..6ba0c35a 100644
--- a/java-api/src/coverage/java/xyz/wagyourtail/jvmdg/coverage/ApiCoverageChecker.java
+++ b/java-api/src/coverage/java/xyz/wagyourtail/jvmdg/coverage/ApiCoverageChecker.java
@@ -92,7 +92,6 @@ public static void main(String[] args) throws IOException, URISyntaxException {
                 System.out.println("Checking version " + stubVersion);
 
                 var unmatchedStubs = versionProvider.stubMappings.values().stream().flatMap(value -> Stream.of(value.getMethodStubMap().values().stream(), value.getMethodModifyMap().values().stream()).flatMap(e -> e)).map(Pair::getFirst).collect(Collectors.toList());
-
                 try {
                     var requiredStubs = new ArrayList<MemberInfo>();
                     compare(versions.get(v), classes, requiredStubs);
@@ -106,7 +105,13 @@ public static void main(String[] args) throws IOException, URISyntaxException {
                         var stub = staticAndStub.fqm();
 
                         if (stub.getName() != null) {
-                            var stubProvider = versionProvider.getStubMapper(stub.getOwner());
+                            Set<String> warnings = new HashSet<>();
+                            var stubProvider = versionProvider.getStubMapper(stub.getOwner(), warnings);
+                            if (!warnings.isEmpty()) {
+                                for (var warning : warnings) {
+                                    System.err.println(warning);
+                                }
+                            }
                             // map classes in desc
                             var desc = stub.getDesc();
                             var descArgs = desc.getArgumentTypes();
@@ -261,6 +266,11 @@ public static ClassNode findClass(String name, List<Path> mods) throws IOExcepti
         return null;
     }
 
+    private static final Set<String> excludedMods = Set.of(
+        "jdk.internal.vm.compiler",
+        "jdk.internal.vm.compiler.management"
+    );
+
     public static void compare(List<Path> moduleHolders, Map<String, Pair<String, ClassNode>> currentVersion, List<MemberInfo> removed) throws IOException {
         var mods = new ArrayList<Path>();
         for (var mod : moduleHolders) {
@@ -276,6 +286,8 @@ public static void compare(List<Path> moduleHolders, Map<String, Pair<String, Cl
                 return;
             }
             var modName = mod.getFileName().toString();
+
+            if (excludedMods.contains(modName)) return;
             try (var files = Files.find(mod, Integer.MAX_VALUE, (p, a) -> !a.isDirectory())) {
                 files.parallel().forEach(p -> {
                     try {
diff --git a/java-api/src/main/java/xyz/wagyourtail/jvmdg/providers/Java8Downgrader.java b/java-api/src/main/java/xyz/wagyourtail/jvmdg/providers/Java8Downgrader.java
index 0f6f5c63..6d28d0c1 100644
--- a/java-api/src/main/java/xyz/wagyourtail/jvmdg/providers/Java8Downgrader.java
+++ b/java-api/src/main/java/xyz/wagyourtail/jvmdg/providers/Java8Downgrader.java
@@ -9,6 +9,7 @@
 import xyz.wagyourtail.jvmdg.j8.stub.*;
 import xyz.wagyourtail.jvmdg.j8.stub.function.*;
 import xyz.wagyourtail.jvmdg.j8.stub.stream.*;
+import xyz.wagyourtail.jvmdg.logging.Logger;
 import xyz.wagyourtail.jvmdg.util.Function;
 import xyz.wagyourtail.jvmdg.util.Pair;
 import xyz.wagyourtail.jvmdg.version.VersionProvider;
@@ -29,7 +30,7 @@ public Java8Downgrader() {
     @Override
     public void ensureInit(ClassDowngrader downgrader) {
         if (!isInitialized()) {
-            if (!downgrader.flags.quiet) System.err.println("[WARNING] Java 8 -> 7 Stubs are VERY incomplete!");
+            if (!downgrader.flags.quiet) downgrader.logger.warn("Java 8 -> 7 Stubs are VERY incomplete!");
         }
         super.ensureInit(downgrader);
     }
@@ -329,12 +330,12 @@ public void init() {
     }
 
     @Override
-    public ClassNode otherTransforms(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly) {
+    public ClassNode otherTransforms(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly, Set<String> warnings) {
         List<ClassNode> classes = new ArrayList<>(extra);
         classes.add(clazz);
         for (ClassNode cls : classes) {
             try {
-                downgradeInterfaces(cls, extra, getReadOnly);
+                downgradeInterfaces(cls, extra, getReadOnly, warnings);
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
@@ -342,15 +343,15 @@ public ClassNode otherTransforms(ClassNode clazz, Set<ClassNode> extra, Function
         return super.otherTransforms(clazz, extra, getReadOnly);
     }
 
-    public void downgradeInterfaces(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly) throws IOException {
+    public void downgradeInterfaces(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly, Set<String> warnings) throws IOException {
         if ((clazz.access & Opcodes.ACC_INTERFACE) != 0) {
-            downgradeInterfaceMethods(clazz, extra, getReadOnly);
+            downgradeInterfaceMethods(clazz, extra, getReadOnly, warnings);
         } else {
-            downgradeInterfaceAccesses(clazz, extra, getReadOnly);
+            downgradeInterfaceAccesses(clazz, extra, getReadOnly, warnings);
         }
     }
 
-    private void downgradeInterfaceMethods(final ClassNode clazz, Set<ClassNode> extra, final Function<String, ClassNode> getReadOnly) throws IOException {
+    private void downgradeInterfaceMethods(final ClassNode clazz, Set<ClassNode> extra, final Function<String, ClassNode> getReadOnly, Set<String> warnings) throws IOException {
         ClassNode interfaceStaticDefaults = new ClassNode();
         boolean removed = false;
         Iterator<MethodNode> mnodes = clazz.methods.iterator();
@@ -410,14 +411,14 @@ public ClassNode apply(String s) {
                     }
                     return getReadOnly.apply(s);
                 }
-            });
+            }, warnings);
         }
     }
 
-    private void downgradeInterfaceAccesses(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly) throws IOException {
+    private void downgradeInterfaceAccesses(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly, Set<String> warnings) throws IOException {
         Map<MemberNameAndDesc, Type> members = new HashMap<>();
         if (!clazz.name.endsWith("$jvmdg$StaticDefaults")) {
-            ClassMapping stubMapper = getStubMapper(Type.getObjectType(clazz.name), (clazz.access & Opcodes.ACC_INTERFACE) != 0);
+            ClassMapping stubMapper = getStubMapper(Type.getObjectType(clazz.name), (clazz.access & Opcodes.ACC_INTERFACE) != 0, warnings);
             for (Map.Entry<MemberNameAndDesc, Pair<Boolean, Type>> member : stubMapper.getMembers().entrySet()) {
                 if (member.getValue().getFirst()) {
                     members.put(member.getKey(), member.getValue().getSecond());
@@ -435,7 +436,7 @@ private void downgradeInterfaceAccesses(ClassNode clazz, Set<ClassNode> extra, F
                                 min.owner.startsWith("com/sun/")
                         ) {
                             if (min.getOpcode() == Opcodes.INVOKESTATIC || min.getOpcode() == Opcodes.INVOKESPECIAL) {
-                                System.err.println("[Java8 Interface Downgrader] Found java interface missing stub: " + FullyQualifiedMemberNameAndDesc.of(min));
+                                warnings.add("Found java interface missing stub: " + FullyQualifiedMemberNameAndDesc.of(min));
                             }
                             continue;
                         }
@@ -466,7 +467,7 @@ private void downgradeInterfaceAccesses(ClassNode clazz, Set<ClassNode> extra, F
                                         handle.getOwner().startsWith("jdk/") ||
                                         handle.getOwner().startsWith("com/sun/")) {
                                     if (handle.getTag() == Opcodes.H_INVOKESTATIC || handle.getTag() == Opcodes.H_INVOKESPECIAL) {
-                                        System.err.println("[Java8 Interface Downgrader] Found java interface missing stub: " + FullyQualifiedMemberNameAndDesc.of(handle));
+                                        warnings.add("Found java interface missing stub: " + FullyQualifiedMemberNameAndDesc.of(handle));
                                     }
                                     continue;
                                 }
@@ -508,7 +509,7 @@ private void downgradeInterfaceAccesses(ClassNode clazz, Set<ClassNode> extra, F
                     internalName.startsWith("sun/") ||
                     internalName.startsWith("jdk/") ||
                     internalName.startsWith("com/sun/")) {
-                System.err.println("[Java8 Interface Downgrader] Found java interface default missing implementation: " + member.getKey().toFullyQualified(member.getValue()));
+                warnings.add("[Java8 Interface Downgrader] Found java interface default missing implementation: " + member.getKey().toFullyQualified(member.getValue()));
                 continue;
             }
             // create method redirecting to static default
diff --git a/site/build.gradle.kts b/site/build.gradle.kts
index d4aa349a..4043fa48 100644
--- a/site/build.gradle.kts
+++ b/site/build.gradle.kts
@@ -1,7 +1,7 @@
 import xyz.wagyourtail.gradle.shadow.ShadowJar
 
 plugins {
-    kotlin("jvm") version "1.9.22"
+    kotlin("jvm")
     application
 }
 
diff --git a/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/html/About.kt b/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/html/About.kt
index 2047408f..53b253d2 100644
--- a/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/html/About.kt
+++ b/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/html/About.kt
@@ -18,28 +18,31 @@ class About : Template<HTML> {
         head {
             title("JvmDowngrader")
             style {
-                +"""
-                    body {
-                        max-width: 800px;
-                        margin:0 auto;
-                    }
-                    h1 {
-                        text-align: center;
-                    }
-                    .user-del {
-                        text-decoration: line-through;
-                    }
-                """.trimIndent()
+                unsafe {
+                    +"""
+                        body {
+                            max-width: 800px;
+                            margin:0 auto;
+                        }
+                        h1 {
+                            text-align: center;
+                        }
+                        .user-del {
+                            text-decoration: line-through;
+                        }
+                    """.trimIndent()
+                }
             }
             link(rel = "stylesheet", href = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css")
             script(src = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js") {}
             script(src = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/gradle.min.js") {}
-            // <script>hljs.highlightAll();</script>
             script {
                 defer = true
-                +"""
-                    hljs.highlightAll();
-                """.trimIndent()
+                unsafe {
+                    +"""
+                        hljs.highlightAll();
+                    """.trimIndent()
+                }
             }
         }
         body {
diff --git a/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/MavenClient.kt b/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/MavenClient.kt
index 75248fce..0ebf636c 100644
--- a/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/MavenClient.kt
+++ b/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/MavenClient.kt
@@ -1,3 +1,5 @@
+@file:Suppress("DEPRECATION")
+
 package xyz.wagyourtail.jvmdg.site.maven
 
 import org.apache.maven.repository.internal.MavenRepositorySystemUtils
@@ -6,16 +8,12 @@ import org.eclipse.aether.RepositorySystem
 import org.eclipse.aether.artifact.DefaultArtifact
 import org.eclipse.aether.collection.CollectRequest
 import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
-import org.eclipse.aether.graph.DefaultDependencyNode
 import org.eclipse.aether.graph.Dependency
 import org.eclipse.aether.impl.DefaultServiceLocator
-import org.eclipse.aether.metadata.DefaultMetadata
-import org.eclipse.aether.metadata.Metadata
 import org.eclipse.aether.repository.LocalRepository
 import org.eclipse.aether.repository.RemoteRepository
 import org.eclipse.aether.resolution.ArtifactRequest
 import org.eclipse.aether.resolution.DependencyRequest
-import org.eclipse.aether.resolution.MetadataRequest
 import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
 import org.eclipse.aether.spi.connector.transport.TransporterFactory
 import org.eclipse.aether.transport.file.FileTransporterFactory
@@ -25,7 +23,6 @@ import org.w3c.dom.Document
 import xyz.wagyourtail.jvmdg.site.maven.impl.ConsoleRepositoryListener
 import xyz.wagyourtail.jvmdg.site.maven.impl.ConsoleTransferListener
 import java.io.File
-import java.io.InputStream
 import java.net.URI
 import java.net.http.HttpClient
 import java.net.http.HttpRequest
diff --git a/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/html/Maven.kt b/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/html/Maven.kt
index aa8d7562..ff102234 100644
--- a/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/html/Maven.kt
+++ b/site/src/main/kotlin/xyz/wagyourtail/jvmdg/site/maven/html/Maven.kt
@@ -10,16 +10,17 @@ class Maven : Template<HTML> {
         head {
             title("JvmDowngrader Maven")
             style {
-                +"""
-                    body {
-                        max-width: 800px;
-                        margin:0 auto;
-                    }
-                    h1 {
-                        text-align: center;
-                    }
-                    
-                """.trimIndent()
+                unsafe {
+                    +"""
+                        body {
+                            max-width: 800px;
+                            margin:0 auto;
+                        }
+                        h1 {
+                            text-align: center;
+                        }
+                    """.trimIndent()
+                }
             }
         }
         body {
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java b/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java
index 12c757f3..e0622f4a 100644
--- a/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java
+++ b/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java
@@ -1,5 +1,6 @@
 package xyz.wagyourtail.jvmdg;
 
+import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.VisibleForTesting;
@@ -14,6 +15,7 @@
 import xyz.wagyourtail.jvmdg.asm.ASMUtils;
 import xyz.wagyourtail.jvmdg.classloader.DowngradingClassLoader;
 import xyz.wagyourtail.jvmdg.cli.Flags;
+import xyz.wagyourtail.jvmdg.logging.Logger;
 import xyz.wagyourtail.jvmdg.util.Function;
 import xyz.wagyourtail.jvmdg.util.Pair;
 import xyz.wagyourtail.jvmdg.util.Utils;
@@ -28,6 +30,7 @@
 import java.net.URLClassLoader;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
 
 public class ClassDowngrader implements Closeable {
     /**
@@ -40,9 +43,13 @@ public class ClassDowngrader implements Closeable {
     private final Map<Integer, VersionProvider> downgraders;
     private final DowngradingClassLoader classLoader;
 
+    @ApiStatus.Internal
+    public final Logger logger;
+
     protected ClassDowngrader(@NotNull Flags flags) {
         this.flags = flags;
         this.target = flags.classVersion;
+        logger = new Logger(ClassDowngrader.class, flags.printDebug ? Logger.Level.DEBUG : flags.quiet ? Logger.Level.FATAL : Logger.Level.INFO, System.out);
         try {
             classLoader = new DowngradingClassLoader(this, ClassDowngrader.class.getClassLoader());
         } catch (IOException e) {
@@ -105,14 +112,14 @@ public synchronized Map<Integer, VersionProvider> collectProviders() {
         return downgraders;
     }
 
-    public Set<MemberNameAndDesc> getMembers(int version, Type type) throws IOException {
+    public Set<MemberNameAndDesc> getMembers(int version, Type type, Set<String> warnings) throws IOException {
         for (int vers = version; vers > target; vers--) {
             VersionProvider downgrader = downgraders.get(vers);
             if (downgrader == null) {
                 throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet());
             }
             downgrader.ensureInit(this);
-            Type stubbed = downgrader.stubClass(type);
+            Type stubbed = downgrader.stubClass(type, warnings);
             if (!stubbed.equals(type)) {
                 try (InputStream stream = classLoader.getResourceAsStream(stubbed.getInternalName() + ".class")) {
                     if (stream == null) throw new IOException("Failed to find stubbed class: " + stubbed);
@@ -122,7 +129,7 @@ public Set<MemberNameAndDesc> getMembers(int version, Type type) throws IOExcept
                     Set<MemberNameAndDesc> members = new HashSet<>();
                     for (MethodNode o : node.methods) {
                         if ((o.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_PRIVATE)) != 0) continue;
-                        members.add(new MemberNameAndDesc(o.name, stubClass(node.version, Type.getMethodType(o.desc))));
+                        members.add(new MemberNameAndDesc(o.name, stubClass(node.version, Type.getMethodType(o.desc), warnings)));
                     }
                     return members;
                 }
@@ -144,20 +151,20 @@ public Set<MemberNameAndDesc> getMembers(int version, Type type) throws IOExcept
             Set<MemberNameAndDesc> members = new HashSet<>();
             for (MethodNode o : node.methods) {
                 if ((o.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_PRIVATE)) != 0) continue;
-                members.add(new MemberNameAndDesc(o.name, stubClass(node.version, Type.getMethodType(o.desc))));
+                members.add(new MemberNameAndDesc(o.name, stubClass(node.version, Type.getMethodType(o.desc), warnings)));
             }
             return members;
         }
     }
 
-    public List<Pair<Type, Boolean>> getSupertypes(int version, Type type) throws IOException {
+    public List<Pair<Type, Boolean>> getSupertypes(int version, Type type, Set<String> warnings) throws IOException {
         for (int vers = version; vers > target; vers--) {
             VersionProvider downgrader = downgraders.get(vers);
             if (downgrader == null) {
                 throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet());
             }
             downgrader.ensureInit(this);
-            Type stubbed = downgrader.stubClass(type);
+            Type stubbed = downgrader.stubClass(type, warnings);
             if (!stubbed.equals(type)) {
                 try (InputStream stream = classLoader.getResourceAsStream(stubbed.getInternalName() + ".class")) {
                     if (stream == null) throw new IOException("Failed to find stubbed class: " + stubbed);
@@ -183,14 +190,14 @@ public List<Pair<Type, Boolean>> getSupertypes(int version, Type type) throws IO
         }
     }
 
-    public Boolean isInterface(int version, Type type) throws IOException {
+    public Boolean isInterface(int version, Type type, Set<String> warnings) throws IOException {
         for (int vers = version; vers > target; vers--) {
             VersionProvider downgrader = downgraders.get(vers);
             if (downgrader == null) {
                 throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet());
             }
             downgrader.ensureInit(this);
-            Type stubbed = downgrader.stubClass(type);
+            Type stubbed = downgrader.stubClass(type, warnings);
             if (!stubbed.equals(type)) {
                 try (InputStream stream = classLoader.getResourceAsStream(stubbed.getInternalName() + ".class")) {
                     if (stream == null) throw new IOException("Failed to find stubbed class: " + stubbed);
@@ -206,14 +213,14 @@ public Boolean isInterface(int version, Type type) throws IOException {
         }
     }
 
-    public Type stubClass(int version, Type type) {
+    public Type stubClass(int version, Type type, Set<String> warnings) {
         for (int vers = version; vers > target; vers--) {
             VersionProvider downgrader = downgraders.get(vers);
             if (downgrader == null) {
                 throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet());
             }
             downgrader.ensureInit(this);
-            Type stubbed = downgrader.stubClass(type);
+            Type stubbed = downgrader.stubClass(type, warnings);
             if (!stubbed.equals(type)) {
                 return stubbed;
             }
@@ -310,12 +317,12 @@ public ClassNode apply(String s) {
         } catch (Exception e) {
             throw new RuntimeException("Failed to downgrade " + name.get(), e);
         }
-        if (flags.printDebug) {
+        if (logger.is(Logger.Level.DEBUG)) {
             for (Map.Entry<String, byte[]> entry : outputs.entrySet()) {
                 if (!entry.getKey().equals(name.get())) {
-                    System.out.println("Downgraded " + entry.getKey() + " from unknown to " + target);
+                    logger.debug("Downgraded " + entry.getKey() + " from unknown to " + target);
                 } else {
-                    System.out.println("Downgraded " + entry.getKey() + " from " + version + " to " + target);
+                    logger.debug("Downgraded " + entry.getKey() + " from " + version + " to " + target);
                 }
                 writeBytesToDebug(entry.getKey(), entry.getValue());
             }
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java b/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java
index bce840e4..ec4ada64 100644
--- a/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java
+++ b/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java
@@ -3,6 +3,7 @@
 import xyz.wagyourtail.jvmdg.ClassDowngrader;
 import xyz.wagyourtail.jvmdg.classloader.providers.ClassLoaderResourceProvider;
 import xyz.wagyourtail.jvmdg.classloader.providers.JarFileResourceProvider;
+import xyz.wagyourtail.jvmdg.logging.Logger;
 import xyz.wagyourtail.jvmdg.util.Function;
 import xyz.wagyourtail.jvmdg.util.Utils;
 
@@ -11,8 +12,6 @@
 import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
-import java.nio.file.FileSystem;
-import java.nio.file.Path;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.jar.JarFile;
@@ -22,6 +21,8 @@ public class DowngradingClassLoader extends ClassLoader implements Closeable {
     private final ClassDowngrader currentVersionDowngrader;
     private final List<ResourceProvider> delegates = new ArrayList<>();
 
+    private final Logger logger;
+
     public DowngradingClassLoader(ClassDowngrader downgrader) throws IOException {
         super();
         File apiJar = downgrader.flags.findJavaApi();
@@ -34,6 +35,7 @@ public DowngradingClassLoader(ClassDowngrader downgrader) throws IOException {
         } else {
             this.currentVersionDowngrader = downgrader;
         }
+        logger = holder.logger.subLogger(DowngradingClassLoader.class);
     }
 
     public DowngradingClassLoader(ClassDowngrader downgrader, ClassLoader parent) throws IOException {
@@ -48,6 +50,7 @@ public DowngradingClassLoader(ClassDowngrader downgrader, ClassLoader parent) th
         } else {
             this.currentVersionDowngrader = downgrader;
         }
+        logger = holder.logger.subLogger(DowngradingClassLoader.class);
     }
 
     public DowngradingClassLoader(ClassDowngrader downgrader, List<ResourceProvider> providers, ClassLoader parent) throws IOException {
@@ -108,8 +111,7 @@ public byte[] apply(String s) {
                 returnValue = defineClass(name, bytes, 0, bytes.length);
             } catch (ClassFormatError e) {
                 currentVersionDowngrader.writeBytesToDebug(name, bytes);
-//                System.err.println("Failed to load class " + name + " with downgraded bytes, writing to debug folder.");
-//                throw e;
+                logger.fatal("Failed to load class " + name + " with downgraded bytes, writing to debug folder.", e);
                 throw new ClassNotFoundException(name, e);
             }
             for (Map.Entry<String, byte[]> entry : outputs.entrySet()) {
@@ -119,7 +121,7 @@ public byte[] apply(String s) {
                 try {
                     defineClass(extraName, extraBytes, 0, extraBytes.length);
                 } catch (ClassFormatError | ClassCircularityError e) {
-                    System.err.println("Failed to load class " + extraName + " with downgraded bytes, writing to debug folder.");
+                    logger.fatal("Failed to load class " + extraName + " with downgraded bytes, writing to debug folder.", e);
                     currentVersionDowngrader.writeBytesToDebug(extraName, bytes);
                     throw e;
                 }
@@ -127,8 +129,7 @@ public byte[] apply(String s) {
             return returnValue;
         } catch (ClassFormatError e) {
             currentVersionDowngrader.writeBytesToDebug(name, bytes);
-//           System.err.println("Failed to load class " + name + " with original bytes, writing to debug folder.");
-//           throw e;
+            logger.fatal("Failed to load class " + name + " with original bytes, writing to debug folder.", e);
             throw new ClassNotFoundException(name, e);
         } catch (Throwable e) {
             throw new ClassNotFoundException(name, e);
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java b/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java
new file mode 100644
index 00000000..4528ec75
--- /dev/null
+++ b/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java
@@ -0,0 +1,179 @@
+package xyz.wagyourtail.jvmdg.logging;
+
+import xyz.wagyourtail.jvmdg.util.Consumer;
+import xyz.wagyourtail.jvmdg.util.Utils;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Iterator;
+
+public class Logger {
+    private final String prefix;
+    private final Level level;
+    private final PrintStream out;
+
+    public Logger(String prefix, Level level, PrintStream out) {
+        this.prefix = prefix;
+        this.level = level;
+        this.out = out;
+    }
+
+    public Logger(Class<?> clazz, Level level, PrintStream out) {
+        this(clazz.getSimpleName(), level, out);
+    }
+
+    public boolean is(Level level) {
+        return level.ordinal() >= this.level.ordinal();
+    }
+
+    public void log(Level level, String message) {
+        if (is(level)) {
+            out.println(level.wrap("[" + prefix + "] " + level + ": " + message));
+        }
+    }
+
+    public void trace(String message) {
+        log(Level.TRACE, message);
+    }
+
+    public void debug(String message) {
+        log(Level.DEBUG, message);
+    }
+
+    public void info(String message) {
+        log(Level.INFO, message);
+    }
+
+    public void warn(String message) {
+        log(Level.WARN, message);
+    }
+
+    public void error(String message) {
+        log(Level.ERROR, message);
+    }
+
+    public void fatal(String message) {
+        log(Level.FATAL, message);
+    }
+
+    public void warn(final String message, final Throwable t) {
+        wrapPrintStream(Level.WARN, new Consumer<PrintStream>() {
+            @Override
+            public void accept(PrintStream printStream) {
+                printStream.println(message);
+                t.printStackTrace(printStream);
+            }
+        });
+    }
+
+    public void error(final String message, final Throwable t) {
+        wrapPrintStream(Level.ERROR, new Consumer<PrintStream>() {
+            @Override
+            public void accept(PrintStream printStream) {
+                printStream.println(message);
+                t.printStackTrace(printStream);
+            }
+        });
+    }
+
+    public void fatal(final String message, final Throwable t) {
+        wrapPrintStream(Level.FATAL, new Consumer<PrintStream>() {
+            @Override
+            public void accept(PrintStream printStream) {
+                printStream.println(message);
+                t.printStackTrace(printStream);
+            }
+        });
+    }
+
+    public Logger subLogger(String prefix) {
+        return new Logger(this.prefix + "/" + prefix, level, out);
+    }
+
+    public Logger subLogger(Class<?> clazz) {
+        return subLogger(clazz.getSimpleName());
+    }
+
+    public void wrapPrintStream(Level level, Consumer<PrintStream> ps) {
+        if (is(level)) {
+            try {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                PrintStream ps2 = new PrintStream(baos, true, StandardCharsets.UTF_8.name());
+                ps.accept(ps2);
+                ps2.close();
+                String str = baos.toString(StandardCharsets.UTF_8.name());
+                log(level, str);
+            } catch (UnsupportedEncodingException e) {
+                Utils.<RuntimeException>sneakyThrow(e);
+            }
+        }
+    }
+
+    public enum Level {
+        TRACE(AnsiColor.DARK_GRAY),
+        DEBUG(AnsiColor.LIGHT_GRAY),
+        INFO(AnsiColor.WHITE),
+        WARN(AnsiColor.YELLOW),
+        ERROR(AnsiColor.RED),
+        FATAL(AnsiColor.LIGHT_RED);
+        ;
+
+        private final AnsiColor ansiColor;
+
+        Level(AnsiColor ansiColor) {
+            this.ansiColor = ansiColor;
+        }
+
+        public AnsiColor getAnsiColor() {
+            return ansiColor;
+        }
+
+        public String wrap(String message) {
+            return ansiColor.wrap(message);
+        }
+    }
+
+    public enum AnsiColor {
+        RESET("\u001B[0m"),
+        BLACK("\u001B[30m"),
+        RED("\u001B[31m"),
+        GREEN("\u001B[32m"),
+        YELLOW("\u001B[33m"),
+        BLUE("\u001B[34m"),
+        PURPLE("\u001B[35m"),
+        CYAN("\u001B[36m"),
+        LIGHT_GRAY("\u001B[37m"),
+
+        DARK_GRAY("\u001B[90m"),
+        LIGHT_RED("\u001B[91m"),
+        LIGHT_GREEN("\u001B[92m"),
+        LIGHT_YELLOW("\u001B[93m"),
+        LIGHT_BLUE("\u001B[94m"),
+        LIGHT_PURPLE("\u001B[95m"),
+        LIGHT_CYAN("\u001B[96m"),
+        WHITE("\u001B[97m");
+
+        private final String ansiColor;
+
+        AnsiColor(String ansiColor) {
+            this.ansiColor = ansiColor;
+        }
+
+        public String getAnsiColor() {
+            return ansiColor;
+        }
+
+        public String wrap(String message) {
+            String[] parts = message.split("\n");
+            StringBuilder sb = new StringBuilder();
+            Iterator<String> it = Arrays.asList(parts).iterator();
+            while (it.hasNext()) {
+                sb.append(ansiColor).append(it.next()).append(RESET.ansiColor);
+                if (it.hasNext()) sb.append("\n");
+            }
+            return sb.toString();
+        }
+
+    }
+}
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/version/Coverage.java b/src/main/java/xyz/wagyourtail/jvmdg/version/Coverage.java
new file mode 100644
index 00000000..5be5742d
--- /dev/null
+++ b/src/main/java/xyz/wagyourtail/jvmdg/version/Coverage.java
@@ -0,0 +1,93 @@
+package xyz.wagyourtail.jvmdg.version;
+
+import org.jetbrains.annotations.Nullable;
+import org.objectweb.asm.Type;
+import xyz.wagyourtail.jvmdg.util.Utils;
+import xyz.wagyourtail.jvmdg.version.map.FullyQualifiedMemberNameAndDesc;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class Coverage {
+    private final VersionProvider versionProvider;
+    private final int javaVersion;
+    private final String path;
+
+    private Map<Type, String> classes;
+    private Map<FullyQualifiedMemberNameAndDesc, String> members;
+
+    public Coverage(int inputVersion, VersionProvider versionProvider) {
+        this.javaVersion = Utils.classVersionToMajorVersion(inputVersion);
+        this.path = "/META-INF/coverage/" + javaVersion+ "/missing.txt";
+        this.versionProvider = versionProvider;
+    }
+
+    private void readLine(String line, Map<Type, String> classes, Map<FullyQualifiedMemberNameAndDesc, String> members) throws IOException {
+        String[] parts = line.split(";", 3);
+        if (parts.length != 3) throw new IOException("Invalid line: " + line);
+        String mod = parts[0];
+        String name = parts[1] + ";";
+        if (parts[2].equals(";")) {
+            classes.put(Type.getType(name), mod);
+        } else {
+            String[] nameAndDesc = parts[2].split(";", 2);
+            if (nameAndDesc.length != 2) throw new IOException("Invalid line: " + line);
+            String desc = nameAndDesc[1].substring(0, nameAndDesc[1].lastIndexOf(";"));
+            members.put(new FullyQualifiedMemberNameAndDesc(Type.getType(name), nameAndDesc[0], Type.getType(desc)), mod);
+        }
+    }
+
+    private synchronized void load() {
+        try {
+            if (classes != null) return;
+            Map<Type, String> classes = new LinkedHashMap<>();
+            Map<FullyQualifiedMemberNameAndDesc, String> members = new LinkedHashMap<>();
+            try (InputStream is = versionProvider.getClass().getResourceAsStream(path)) {
+                if (is != null) {
+                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
+                        String line;
+                        while ((line = reader.readLine()) != null) {
+                            readLine(line, classes, members);
+                        }
+                    }
+                }
+            }
+            this.classes = classes;
+            this.members = members;
+        } catch (IOException e) {
+            Utils.<RuntimeException>sneakyThrow(e);
+        }
+    }
+
+    @Nullable
+    public String checkClass(Type type) {
+        load();
+        return classes.get(type);
+    }
+
+    @Nullable
+    public String checkMember(FullyQualifiedMemberNameAndDesc member) {
+        load();
+        return members.get(member);
+    }
+
+    public void warnClass(Type type, Set<String> warnings) {
+        String mod = checkClass(type);
+        if (mod != null) {
+            warnings.add("Class " + type.getClassName() + " from " + mod + " in " + javaVersion + " is not covered by the api stubs");
+        }
+    }
+
+    public void warnMember(FullyQualifiedMemberNameAndDesc member, Set<String> warnings) {
+        String mod = checkMember(member);
+        if (mod != null) {
+            warnings.add("Member " + member + " from " + mod + " in " + javaVersion + " is not covered by the api stubs");
+        }
+    }
+
+}
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java b/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java
index 96342e6c..392f4f05 100644
--- a/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java
+++ b/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java
@@ -7,6 +7,7 @@
 import xyz.wagyourtail.jvmdg.ClassDowngrader;
 import xyz.wagyourtail.jvmdg.all.RemovedInterfaces;
 import xyz.wagyourtail.jvmdg.exc.MissingStubError;
+import xyz.wagyourtail.jvmdg.logging.Logger;
 import xyz.wagyourtail.jvmdg.util.Function;
 import xyz.wagyourtail.jvmdg.util.IOFunction;
 import xyz.wagyourtail.jvmdg.util.Lazy;
@@ -24,27 +25,26 @@
 import java.util.*;
 
 public abstract class VersionProvider {
-
-    public final Map<Type, Pair<Type, Pair<Class<?>, Adapter>>> classStubs = new HashMap<>();
-    public final Map<Type, ClassMapping> stubMappings = new HashMap<>();
+    public final Map<Type, Pair<Type, Pair<Class<?>, Adapter>>> classStubs = new LinkedHashMap<>();
+    public final Map<Type, ClassMapping> stubMappings = new LinkedHashMap<>();
     public final int inputVersion;
     public final int outputVersion;
 
+    public final Coverage coverage;
+
     /**
      * lateinit
      * bound during ensureInit
      */
     protected ClassDowngrader downgrader;
+    protected Logger logger;
 
     private volatile boolean initialized = false;
 
     protected VersionProvider(int inputVersion, int outputVersion) {
         this.inputVersion = inputVersion;
         this.outputVersion = outputVersion;
-    }
-
-    public static void main(String[] args) {
-        System.out.println(Type.getType(boolean.class).getDescriptor());
+        this.coverage = new Coverage(inputVersion, this);
     }
 
     public FullyQualifiedMemberNameAndDesc resolveStubTarget(Member member, Ref ref) {
@@ -152,22 +152,24 @@ public static FullyQualifiedMemberNameAndDesc resolveModifyTarget(Member member,
     }
 
     public void afterInit() {
-        if (downgrader.flags.printDebug) {
+        if (logger.is(Logger.Level.DEBUG)) {
+            StringBuilder sb = new StringBuilder("Stubs: \n");
             for (Map.Entry<Type, Pair<Type, Pair<Class<?>, Adapter>>> stub : classStubs.entrySet()) {
-                System.out.println(stub.getKey().getInternalName() + " -> " + stub.getValue().getFirst());
+                sb.append(stub.getKey().getInternalName()).append(" -> ").append(stub.getValue().getFirst()).append("\n");
             }
             for (Map.Entry<Type, ClassMapping> entry : stubMappings.entrySet()) {
                 for (Map.Entry<MemberNameAndDesc, Pair<Method, Stub>> member : entry.getValue().getMethodStubMap().entrySet()) {
-                    System.out.println(entry.getKey().getInternalName() + "." + member.getKey().getName() + member.getKey().getDesc() + " -> " + member.getValue().getFirst().getDeclaringClass().getCanonicalName().replace('.', '/') + ";" + member.getValue().getFirst().getName() + Type.getMethodDescriptor(member.getValue().getFirst()));
+                    sb.append(entry.getKey().getInternalName())
+                        .append(".").append(member.getKey().getName())
+                        .append(member.getKey().getDesc()).append(" -> ")
+                        .append(member.getValue().getFirst().getDeclaringClass().getCanonicalName().replace('.', '/'))
+                        .append(";")
+                        .append(member.getValue().getFirst().getName())
+                        .append(Type.getMethodDescriptor(member.getValue().getFirst()))
+                        .append("\n");
                 }
             }
-//            for (Map.Entry<Type, Pair<Type, Stub>> entry : classStubs.entrySet()) {
-//                System.out.println(entry.getKey().getInternalName() + " -> " + entry.getValue().getFirst().getInternalName());
-//            }
-//            for (Map.Entry<String, Pair<Method, Stub>> entry : methodStubs.entrySet()) {
-//                System.out.println(entry.getKey() + " -> " + entry.getValue().getFirst().getDeclaringClass().getCanonicalName().replace('.', '/') + ";" + entry.getValue().getFirst().getName() + Type.getMethodDescriptor(entry.getValue()
-//                    .getFirst()));
-//            }
+            logger.debug(sb.toString());
         }
     }
 
@@ -179,27 +181,27 @@ public void preInit() {
 
     public abstract void init();
 
-    public synchronized ClassMapping getStubMapper(Type type) throws IOException {
-        return getStubMapper(type, downgrader.isInterface(outputVersion, type) == Boolean.TRUE);
+    public synchronized ClassMapping getStubMapper(Type type, Set<String> warnings) throws IOException {
+        return getStubMapper(type, downgrader.isInterface(outputVersion, type, warnings) == Boolean.TRUE, warnings);
     }
 
-    public synchronized ClassMapping getStubMapper(Type type, boolean isInterface) throws IOException {
+    public synchronized ClassMapping getStubMapper(Type type, boolean isInterface, final Set<String> warnings) throws IOException {
         return getStubMapper(type, isInterface, new IOFunction<Type, Set<MemberNameAndDesc>>() {
 
             @Override
             public Set<MemberNameAndDesc> apply(Type o) throws IOException {
-                return downgrader.getMembers(inputVersion, o);
+                return downgrader.getMembers(inputVersion, o, warnings);
             }
 
-        });
+        }, warnings);
     }
 
-    public synchronized ClassMapping getStubMapper(Type type, final boolean isInterface, final IOFunction<Type, Set<MemberNameAndDesc>> memberResolver) throws IOException {
+    public synchronized ClassMapping getStubMapper(Type type, final boolean isInterface, final IOFunction<Type, Set<MemberNameAndDesc>> memberResolver, final Set<String> warnings) throws IOException {
         return getStubMapper(type, isInterface, memberResolver, new IOFunction<Type, List<Pair<Type, Boolean>>>() {
 
             @Override
             public List<Pair<Type, Boolean>> apply(Type o) throws IOException {
-                return downgrader.getSupertypes(inputVersion, o);
+                return downgrader.getSupertypes(inputVersion, o, warnings);
             }
 
         });
@@ -210,7 +212,7 @@ public synchronized ClassMapping getStubMapper(final Type type, final boolean is
             return stubMappings.get(type);
         }
         if (type.getSort() == Type.ARRAY) {
-            return new ClassMapping(new Lazy<List<ClassMapping>>() {
+            return new ClassMapping(coverage, new Lazy<List<ClassMapping>>() {
                 @Override
                 public List<ClassMapping> init() {
                     try {
@@ -222,7 +224,7 @@ public List<ClassMapping> init() {
             }, type, isInterface, memberResolver, this);
         }
         if (type.getInternalName().equals("java/lang/Object")) {
-            ClassMapping mapping = new ClassMapping(new Lazy<List<ClassMapping>>() {
+            ClassMapping mapping = new ClassMapping(coverage, new Lazy<List<ClassMapping>>() {
                 @Override
                 public List<ClassMapping> init() {
                     return Collections.emptyList();
@@ -231,14 +233,13 @@ public List<ClassMapping> init() {
             stubMappings.put(type, mapping);
             return mapping;
         }
-        ClassMapping mapping = new ClassMapping(new Lazy<List<ClassMapping>>() {
+        ClassMapping mapping = new ClassMapping(coverage, new Lazy<List<ClassMapping>>() {
             @Override
             public List<ClassMapping> init() {
                 try {
                     List<Pair<Type, Boolean>> types = superTypeResolver.apply(type);
                     if (types == null) {
-                        if (!downgrader.flags.quiet)
-                            System.err.println(VersionProvider.this.getClass().getName() + " Could not find class " + type.getInternalName());
+                        logger.error("Could not find class " + type.getInternalName());
                         types = Collections.emptyList();
                     }
                     List<ClassMapping> superTypes = new ArrayList<>();
@@ -256,6 +257,7 @@ public List<ClassMapping> init() {
     }
 
     public void stub(Class<?> clazz) {
+        Set<String> warnings = new LinkedHashSet<>();
         try {
             if (clazz.isAnnotationPresent(Adapter.class)) {
                 Adapter stub = clazz.getAnnotation(Adapter.class);
@@ -292,7 +294,7 @@ public void stub(Class<?> clazz) {
                             FullyQualifiedMemberNameAndDesc target = resolveStubTarget(method, stub.ref());
                             Type owner = target.getOwner();
                             MemberNameAndDesc member = target.toMemberNameAndDesc();
-                            getStubMapper(owner).addStub(member, method, stub);
+                            getStubMapper(owner, warnings).addStub(member, method, stub);
                         } else if (method.isAnnotationPresent(Modify.class)) {
                             Modify modify = method.getAnnotation(Modify.class);
                             FullyQualifiedMemberNameAndDesc target = resolveModifyTarget(method, modify.ref());
@@ -308,20 +310,14 @@ public void stub(Class<?> clazz) {
                                     throw new IllegalArgumentException("Class " + clazz.getName() + ", @Modify method " + method.getName() + " parameter " + i + " must be of type " + Modify.MODIFY_SIG[i].getName());
                                 }
                             }
-                            getStubMapper(owner).addModify(member, method, modify);
+                            getStubMapper(owner, warnings).addModify(member, method, modify);
                         }
                     } catch (Throwable e) {
-                        if (!downgrader.flags.quiet) {
-                            System.out.println("ERROR: failed to create stub for " + clazz.getName() + " (" + e.getMessage().split("\n")[0] + ")");
-                            e.printStackTrace(System.err);
-                        }
+                        logger.warn("failed to create stub for " + clazz.getName(), e);
                     }
                 }
             } catch (Throwable e) {
-                if (!downgrader.flags.quiet) {
-                    System.out.println("ERROR: failed to resolve methods for " + clazz.getName());
-                    e.printStackTrace(System.err);
-                }
+                logger.warn("failed to resolve methods for " + clazz.getName(), e);
             }
             try {
                 // inner classes
@@ -329,20 +325,21 @@ public void stub(Class<?> clazz) {
                     stub(inner);
                 }
             } catch (Throwable e) {
-                if (!downgrader.flags.quiet) {
-                    System.out.println("ERROR: failed to resolve inner classes for " + clazz.getName());
-                    e.printStackTrace(System.err);
-                }
+                logger.warn("failed to resolve inner classes for " + clazz.getName(), e);
             }
         } catch (Throwable e) {
-            if (!downgrader.flags.quiet) {
-                System.out.println("ERROR: failed to create stub(s) for " + clazz.getName());
-                e.printStackTrace(System.err);
+            logger.warn("failed to create stub(s) for " + clazz.getName(), e);
+        }
+        if (!warnings.isEmpty() && logger.is(Logger.Level.WARN)) {
+            StringBuilder sb = new StringBuilder();
+            for (String warning : warnings) {
+                sb.append("    ").append(warning).append("\n");
             }
+            logger.warn("Warnings for " + clazz.getName() + " (" + warnings.size() + ") : \n" + sb);
         }
     }
 
-    public MethodInsnNode stubTypeInsnNode(TypeInsnNode insn) {
+    public MethodInsnNode stubTypeInsnNode(TypeInsnNode insn, Set<String> warnings) {
         Type desc = Type.getObjectType(insn.desc);
         switch (desc.getSort()) {
             case Type.ARRAY:
@@ -350,10 +347,12 @@ public MethodInsnNode stubTypeInsnNode(TypeInsnNode insn) {
                 if (classStubs.containsKey(type)) {
                     type = classStubs.get(type).getFirst();
                 }
+                coverage.warnClass(type, warnings);
                 insn.desc = Type.getType(desc.getDescriptor().substring(0, desc.getDimensions()) + type.getDescriptor()).getInternalName();
                 return null;
             case Type.OBJECT:
-                if (classStubs.containsKey(Type.getObjectType(insn.desc))) {
+                Type t = Type.getObjectType(insn.desc);
+                if (classStubs.containsKey(t)) {
                     Pair<Type, Pair<Class<?>, Adapter>> stub = classStubs.get(Type.getObjectType(insn.desc));
                     // check if clazz has method `jvmdg$opcode
                     switch (insn.getOpcode()) {
@@ -374,21 +373,23 @@ public MethodInsnNode stubTypeInsnNode(TypeInsnNode insn) {
                         default:
                             insn.desc = stub.getFirst().getInternalName();
                     }
+                    return null;
                 }
+                coverage.warnClass(t, warnings);
                 break;
         }
         return null;
     }
 
-    public Type stubClass(Type desc) {
+    public Type stubClass(Type desc, Set<String> warnings) {
         switch (desc.getSort()) {
             case Type.METHOD:
                 Type[] args = desc.getArgumentTypes();
                 Type ret = desc.getReturnType();
                 for (int i = 0; i < args.length; i++) {
-                    args[i] = stubClass(args[i]);
+                    args[i] = stubClass(args[i], warnings);
                 }
-                ret = stubClass(ret);
+                ret = stubClass(ret, warnings);
                 return Type.getMethodType(ret, args);
             case Type.ARRAY:
                 Type type = desc.getElementType();
@@ -400,6 +401,7 @@ public Type stubClass(Type desc) {
                 if (classStubs.containsKey(desc)) {
                     return classStubs.get(desc).getFirst();
                 }
+//                coverage.warnClass(desc, warnings);
                 return desc;
             default:
                 return desc;
@@ -412,55 +414,63 @@ public ClassNode stubMethods(ClassNode owner, Set<ClassNode> extra, boolean enab
         }
 
         for (MethodNode method : new ArrayList<>(owner.methods)) {
-            MethodNode newMethod = stubMethods(method, owner, extra, enableRuntime, memberResolver, superTypeResolver);
+            Set<String> warnings = new LinkedHashSet<>();
+            MethodNode newMethod = stubMethod(method, owner, extra, enableRuntime, memberResolver, superTypeResolver, warnings);
             if (newMethod != method) {
                 owner.methods.set(owner.methods.indexOf(method), newMethod);
             }
+            if (!warnings.isEmpty() && logger.is(Logger.Level.WARN)) {
+                StringBuilder sb = new StringBuilder();
+                for (String warning : warnings) {
+                    sb.append("    ").append(warning).append("\n");
+                }
+                logger.warn("Warnings for " + owner.name + "." + method.name + method.desc + " (" + warnings.size() + ") : \n" + sb);
+            }
         }
         return owner;
     }
 
-    public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode> extra, boolean enableRuntime, IOFunction<Type, Set<MemberNameAndDesc>> memberResolver, IOFunction<Type, List<Pair<Type, Boolean>>> superTypeResolver) throws IOException {
+    public MethodNode stubMethod(MethodNode method, ClassNode owner, Set<ClassNode> extra, boolean enableRuntime, IOFunction<Type, Set<MemberNameAndDesc>> memberResolver, IOFunction<Type, List<Pair<Type, Boolean>>> superTypeResolver, Set<String> warnings) throws IOException {
         for (int i = 0; i < method.instructions.size(); i++) {
             AbstractInsnNode insn = method.instructions.get(i);
             if (insn instanceof MethodInsnNode) {
                 MethodInsnNode min = (MethodInsnNode) insn;
-                min.owner = stubClass(Type.getObjectType(min.owner)).getInternalName();
-                min.desc = stubClass(Type.getMethodType(min.desc)).getDescriptor();
+                min.owner = stubClass(Type.getObjectType(min.owner), warnings).getInternalName();
+                min.desc = stubClass(Type.getMethodType(min.desc), warnings).getDescriptor();
                 if (!min.owner.startsWith("[")) {
-                    getStubMapper(Type.getObjectType(min.owner), min.itf, memberResolver, superTypeResolver).transform(method, i, owner, extra, enableRuntime);
+                    getStubMapper(Type.getObjectType(min.owner), min.itf, memberResolver, superTypeResolver).transform(method, i, owner, extra, enableRuntime, warnings);
                 }
             } else if (insn instanceof TypeInsnNode) {
                 TypeInsnNode tin = (TypeInsnNode) insn;
-                MethodInsnNode min = stubTypeInsnNode(tin);
+                MethodInsnNode min = stubTypeInsnNode(tin, warnings);
                 if (min != null) {
                     method.instructions.set(tin, min);
                 }
             } else if (insn instanceof FieldInsnNode) {
                 FieldInsnNode fin = (FieldInsnNode) insn;
-                fin.owner = stubClass(Type.getObjectType(fin.owner)).getInternalName();
-                fin.desc = stubClass(Type.getType(fin.desc)).getDescriptor();
+                fin.owner = stubClass(Type.getObjectType(fin.owner), warnings).getInternalName();
+                fin.desc = stubClass(Type.getType(fin.desc), warnings).getDescriptor();
                 //TODO: field stubs (upgrade to method)
             } else if (insn instanceof InvokeDynamicInsnNode) {
                 InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn;
-                indy.desc = stubClass(Type.getMethodType(indy.desc)).getDescriptor();
+                indy.desc = stubClass(Type.getMethodType(indy.desc), warnings).getDescriptor();
                 indy.bsm = new Handle(
-                        indy.bsm.getTag(),
-                        stubClass(Type.getObjectType(indy.bsm.getOwner())).getInternalName(),
-                        indy.bsm.getName(),
-                        stubClass(Type.getMethodType(indy.bsm.getDesc())).getDescriptor(),
-                        indy.bsm.isInterface()
+                    indy.bsm.getTag(),
+                    stubClass(Type.getObjectType(indy.bsm.getOwner()), warnings).getInternalName(),
+                    indy.bsm.getName(),
+                    stubClass(Type.getMethodType(indy.bsm.getDesc()), warnings).getDescriptor(),
+                    indy.bsm.isInterface()
                 );
                 for (int j = 0; j < indy.bsmArgs.length; j++) {
                     Object arg = indy.bsmArgs[j];
                     if (arg instanceof Handle) {
                         Handle handle = (Handle) arg;
                         handle = new Handle(
-                                handle.getTag(),
-                                stubClass(Type.getObjectType(handle.getOwner())).getInternalName(),
-                                handle.getName(),
-                                stubClass(Type.getType(handle.getDesc())).getDescriptor(),
-                                handle.isInterface()
+                            handle.getTag(),
+                            stubClass(Type.getObjectType(handle.getOwner()), warnings).getInternalName(),
+                            handle.getName(),
+                            stubClass(Type.getType(handle.getDesc()), warnings).getDescriptor(),
+                            handle.isInterface()
                         );
                         indy.bsmArgs[j] = handle;
                         switch (handle.getTag()) {
@@ -488,10 +498,10 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                                 ClassMapping stubMapper = getStubMapper(hOwner, handle.isInterface(), memberResolver, superTypeResolver);
                                 boolean isStatic = handle.getTag() == Opcodes.H_INVOKESTATIC;
                                 boolean isSpecial = handle.getTag() == Opcodes.H_INVOKESPECIAL || handle.getTag() == Opcodes.H_NEWINVOKESPECIAL;
-                                Pair<Method, Stub> min = stubMapper.getStubFor(member, isStatic, enableRuntime, isSpecial);
+                                Pair<Method, Stub> min = stubMapper.getStubFor(member, isStatic, enableRuntime, isSpecial, warnings);
                                 if (min != null) {
                                     if (min.getSecond().downgradeVersion()) {
-                                        System.err.println("Invalid stub for indy handle: " + handle.getOwner() + "." + handle.getName() + handle.getDesc());
+                                        warnings.add("Invalid stub for indy handle: " + handle.getOwner() + "." + handle.getName() + handle.getDesc());
                                     } else if (!min.getSecond().abstractDefault()) {
                                         Type hStaticDesc;
                                         if (isStatic) {
@@ -523,10 +533,10 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                                                 if (mn instanceof HandleMethodNode) {
                                                     Handle h = ((HandleMethodNode) mn).ref;
                                                     if (h.getTag() == handle.getTag() &&
-                                                            h.getOwner().equals(handle.getOwner()) &&
-                                                            h.getName().equals(handle.getName()) &&
-                                                            h.getDesc().equals(handle.getDesc()) &&
-                                                            h.isInterface() == handle.isInterface()) {
+                                                        h.getOwner().equals(handle.getOwner()) &&
+                                                        h.getName().equals(handle.getName()) &&
+                                                        h.getDesc().equals(handle.getDesc()) &&
+                                                        h.isInterface() == handle.isInterface()) {
                                                         if (!mn.desc.equals(hStaticDesc.getDescriptor())) {
                                                             num++;
                                                         } else {
@@ -564,18 +574,18 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                                             }
                                         }
                                         indy.bsmArgs[j] = new Handle(
-                                                Opcodes.H_INVOKESTATIC,
-                                                newOwner,
-                                                name,
-                                                desc,
-                                                intf
+                                            Opcodes.H_INVOKESTATIC,
+                                            newOwner,
+                                            name,
+                                            desc,
+                                            intf
                                         );
                                     }
                                 } else {
-                                    Pair<Method, Modify> mod = stubMapper.getModifyFor(member, isStatic);
+                                    Pair<Method, Modify> mod = stubMapper.getModifyFor(member, isStatic, warnings);
                                     if (mod != null) {
                                         if (handle.getTag() != Opcodes.H_NEWINVOKESPECIAL) {
-                                            System.err.println("Invalid modify for indy handle: " + handle.getOwner() + "." + handle.getName() + handle.getDesc());
+                                            warnings.add("Invalid modify for indy handle: " + handle.getOwner() + "." + handle.getName() + handle.getDesc());
                                         } else {
                                             Type returnType = Type.getObjectType(handle.getOwner());
                                             Type[] arguments = hDesc.getArgumentTypes();
@@ -590,10 +600,10 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                                                 if (mn instanceof HandleMethodNode) {
                                                     Handle h = ((HandleMethodNode) mn).ref;
                                                     if (h.getTag() == handle.getTag() &&
-                                                            h.getOwner().equals(handle.getOwner()) &&
-                                                            h.getName().equals(handle.getName()) &&
-                                                            h.getDesc().equals(handle.getDesc()) &&
-                                                            h.isInterface() == handle.isInterface()) {
+                                                        h.getOwner().equals(handle.getOwner()) &&
+                                                        h.getName().equals(handle.getName()) &&
+                                                        h.getDesc().equals(handle.getDesc()) &&
+                                                        h.isInterface() == handle.isInterface()) {
                                                         if (!mn.desc.equals(desc)) {
                                                             num++;
                                                         } else {
@@ -641,11 +651,11 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                                             }
 
                                             indy.bsmArgs[j] = new Handle(
-                                                    Opcodes.H_INVOKESTATIC,
-                                                    owner.name,
-                                                    name,
-                                                    desc,
-                                                    intf
+                                                Opcodes.H_INVOKESTATIC,
+                                                owner.name,
+                                                name,
+                                                desc,
+                                                intf
                                             );
 
                                         }
@@ -656,17 +666,17 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
 
                     } else if (arg instanceof Type) {
                         Type type = (Type) arg;
-                        indy.bsmArgs[j] = stubClass(type);
+                        indy.bsmArgs[j] = stubClass(type, warnings);
                     }
                 }
-                getStubMapper(Type.getObjectType(indy.bsm.getOwner()), indy.bsm.isInterface(), memberResolver, superTypeResolver).transform(method, i, owner, extra, enableRuntime);
+                getStubMapper(Type.getObjectType(indy.bsm.getOwner()), indy.bsm.isInterface(), memberResolver, superTypeResolver).transform(method, i, owner, extra, enableRuntime, warnings);
             } else if (insn instanceof MultiANewArrayInsnNode) {
                 MultiANewArrayInsnNode manain = (MultiANewArrayInsnNode) insn;
-                manain.desc = stubClass(Type.getType(manain.desc)).getDescriptor();
+                manain.desc = stubClass(Type.getType(manain.desc), warnings).getDescriptor();
             } else if (insn instanceof LdcInsnNode) {
                 LdcInsnNode ldc = (LdcInsnNode) insn;
                 if (ldc.cst instanceof Type) {
-                    ldc.cst = stubClass((Type) ldc.cst);
+                    ldc.cst = stubClass((Type) ldc.cst, warnings);
                 } else if (ldc.cst instanceof ConstantDynamic) {
                     ConstantDynamic condy = (ConstantDynamic) ldc.cst;
                     Handle bsm = condy.getBootstrapMethod();
@@ -678,7 +688,7 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                     for (int j = 0; j < fn.local.size(); j++) {
                         Object o = fn.local.get(j);
                         if (o instanceof String) {
-                            fn.local.set(j, stubClass(Type.getObjectType((String) o)).getInternalName());
+                            fn.local.set(j, stubClass(Type.getObjectType((String) o), warnings).getInternalName());
                         }
                     }
                 }
@@ -686,7 +696,7 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
                     for (int j = 0; j < fn.stack.size(); j++) {
                         Object o = fn.stack.get(j);
                         if (o instanceof String) {
-                            fn.stack.set(j, stubClass(Type.getObjectType((String) o)).getInternalName());
+                            fn.stack.set(j, stubClass(Type.getObjectType((String) o), warnings).getInternalName());
                         }
                     }
                 }
@@ -697,6 +707,7 @@ public MethodNode stubMethods(MethodNode method, ClassNode owner, Set<ClassNode>
 
     public void ensureInit(ClassDowngrader downgrader) {
         this.downgrader = downgrader;
+        this.logger = downgrader.logger.subLogger(getClass());
         if (!initialized) {
             synchronized (this) {
                 if (!initialized) {
@@ -722,11 +733,12 @@ public ClassNode downgrade(final ClassDowngrader downgrader, ClassNode clazz, fi
             throw new IllegalArgumentException("Class " + clazz.name + " is not version " + inputVersion);
 
         ensureInit(downgrader);
+        final Set<String> warnings = new LinkedHashSet<>();
 
         final IOFunction<Type, Set<MemberNameAndDesc>> getMembers = new IOFunction<Type, Set<MemberNameAndDesc>>() {
             @Override
             public Set<MemberNameAndDesc> apply(Type o) throws IOException {
-                Set<MemberNameAndDesc> members = downgrader.getMembers(inputVersion, o);
+                Set<MemberNameAndDesc> members = downgrader.getMembers(inputVersion, o, warnings);
                 // if not found in the classloader, check getReadOnly for it. this should really only happen with the ZipDowngrader
                 if (members == null) {
                     ClassNode ro = getReadOnly.apply(o.getInternalName());
@@ -734,7 +746,7 @@ public Set<MemberNameAndDesc> apply(Type o) throws IOException {
                         members = new HashSet<>();
                         for (MethodNode method : ro.methods) {
                             if ((method.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_PRIVATE)) != 0) continue;
-                            members.add(new MemberNameAndDesc(method.name, downgrader.stubClass(ro.version, Type.getMethodType(method.desc))));
+                            members.add(new MemberNameAndDesc(method.name, downgrader.stubClass(ro.version, Type.getMethodType(method.desc), warnings)));
                         }
                     }
                 }
@@ -745,7 +757,7 @@ public Set<MemberNameAndDesc> apply(Type o) throws IOException {
                             members = new HashSet<>();
                             for (MethodNode method : extraClass.methods) {
                                 if ((method.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_PRIVATE)) != 0) continue;
-                                members.add(new MemberNameAndDesc(method.name, downgrader.stubClass(extraClass.version, Type.getMethodType(method.desc))));
+                                members.add(new MemberNameAndDesc(method.name, downgrader.stubClass(extraClass.version, Type.getMethodType(method.desc), warnings)));
                             }
                         }
                     }
@@ -758,15 +770,15 @@ public Set<MemberNameAndDesc> apply(Type o) throws IOException {
 
             @Override
             public List<Pair<Type, Boolean>> apply(Type o) throws IOException {
-                List<Pair<Type, Boolean>> types = downgrader.getSupertypes(inputVersion, o);
+                List<Pair<Type, Boolean>> types = downgrader.getSupertypes(inputVersion, o, warnings);
                 // if not found in the classloader, check getReadOnly for it. this should really only happen with the ZipDowngrader
                 if (types == null) {
                     ClassNode ro = getReadOnly.apply(o.getInternalName());
                     if (ro != null) {
                         types = new ArrayList<>();
-                        types.add(new Pair<>(downgrader.stubClass(ro.version, Type.getObjectType(ro.superName)), Boolean.FALSE));
+                        types.add(new Pair<>(downgrader.stubClass(ro.version, Type.getObjectType(ro.superName), warnings), Boolean.FALSE));
                         for (String anInterface : ro.interfaces) {
-                            types.add(new Pair<>(downgrader.stubClass(ro.version, Type.getObjectType(anInterface)), Boolean.TRUE));
+                            types.add(new Pair<>(downgrader.stubClass(ro.version, Type.getObjectType(anInterface), warnings), Boolean.TRUE));
                         }
                     }
                 }
@@ -775,9 +787,9 @@ public List<Pair<Type, Boolean>> apply(Type o) throws IOException {
                     for (ClassNode extraClass : extra) {
                         if (extraClass.name.equals(o.getInternalName())) {
                             types = new ArrayList<>();
-                            types.add(new Pair<>(downgrader.stubClass(extraClass.version, Type.getObjectType(extraClass.superName)), Boolean.FALSE));
+                            types.add(new Pair<>(downgrader.stubClass(extraClass.version, Type.getObjectType(extraClass.superName), warnings), Boolean.FALSE));
                             for (String anInterface : extraClass.interfaces) {
-                                types.add(new Pair<>(downgrader.stubClass(extraClass.version, Type.getObjectType(anInterface)), Boolean.TRUE));
+                                types.add(new Pair<>(downgrader.stubClass(extraClass.version, Type.getObjectType(anInterface), warnings), Boolean.TRUE));
                             }
                         }
                     }
@@ -786,33 +798,56 @@ public List<Pair<Type, Boolean>> apply(Type o) throws IOException {
             }
         };
 
-        clazz = stubClasses(clazz, enableRuntime);
-        if (clazz == null) return null;
+        clazz = stubClasses(clazz, enableRuntime, warnings);
+        if (clazz == null) {
+            printWarnings(warnings, clazz.name);
+            return null;
+        }
         clazz = stubWithExtras(clazz, extra, new IOFunction<ClassNode, ClassNode>() {
             @Override
             public ClassNode apply(ClassNode classNode) throws IOException {
                 return stubMethods(classNode, extra, enableRuntime, getMembers, getSuperTypes);
             }
         });
-        if (clazz == null) return null;
+        if (clazz == null) {
+            printWarnings(warnings, clazz.name);
+            return null;
+        }
         clazz = stubWithExtras(clazz, extra, new IOFunction<ClassNode, ClassNode>() {
             @Override
             public ClassNode apply(ClassNode classNode) throws IOException {
                 return insertAbstractMethods(classNode, extra, getMembers, getSuperTypes);
             }
         });
-        if (clazz == null) return null;
+        if (clazz == null) {
+            printWarnings(warnings, clazz.name);
+            return null;
+        }
         clazz = stubWithExtras(clazz, extra, new IOFunction<ClassNode, ClassNode>() {
             @Override
             public ClassNode apply(ClassNode classNode) throws IOException {
-                return otherTransforms(classNode, extra, getReadOnly);
+                return otherTransforms(classNode, extra, getReadOnly, warnings);
             }
         });
-        if (clazz == null) return null;
+        if (clazz == null) {
+            printWarnings(warnings, clazz.name);
+            return null;
+        }
+        printWarnings(warnings, clazz.name);
         clazz.version = inputVersion - 1;
         return clazz;
     }
 
+    private void printWarnings(Set<String> warnings, String className) {
+        if (!warnings.isEmpty() && logger.is(Logger.Level.WARN)) {
+            StringBuilder sb = new StringBuilder();
+            for (String warning : warnings) {
+                sb.append("    ").append(warning).append("\n");
+            }
+            logger.warn("Warnings for " + className + " (" + warnings.size() + ") : \n" + sb);
+        }
+    }
+
     public ClassNode stubWithExtras(ClassNode clazz, Set<ClassNode> extra, IOFunction<ClassNode, ClassNode> stubber) throws IOException {
         clazz = stubber.apply(clazz);
         if (clazz == null) return null;
@@ -862,6 +897,11 @@ public ClassNode insertAbstractMethods(ClassNode clazz, Set<ClassNode> extra, IO
         return clazz;
     }
 
+    public ClassNode otherTransforms(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly, Set<String> warnings) {
+        clazz = otherTransforms(clazz, extra, getReadOnly);
+        return clazz;
+    }
+
     public ClassNode otherTransforms(ClassNode clazz, Set<ClassNode> extra, Function<String, ClassNode> getReadOnly) {
         clazz = otherTransforms(clazz, extra);
         return clazz;
@@ -876,7 +916,7 @@ public ClassNode otherTransforms(ClassNode clazz) {
         return clazz;
     }
 
-    public ClassNode stubClasses(ClassNode clazz, boolean enableRuntime) {
+    public ClassNode stubClasses(ClassNode clazz, boolean enableRuntime, Set<String> warnings) {
         if (clazz.name.equals("module-info")) {
             return clazz;
         }
@@ -928,16 +968,16 @@ public ClassNode stubClasses(ClassNode clazz, boolean enableRuntime) {
 
         // signature
         if (clazz.signature != null) {
-            clazz.signature = transformSignature(clazz.signature);
+            clazz.signature = transformSignature(clazz.signature, warnings);
         }
 
         // field descriptor
         if (clazz.fields != null) {
             for (FieldNode field : clazz.fields) {
                 type = Type.getType(field.desc);
-                field.desc = stubClass(type).getDescriptor();
+                field.desc = stubClass(type, warnings).getDescriptor();
                 if (field.signature != null) {
-                    field.signature = transformSignature(field.signature);
+                    field.signature = transformSignature(field.signature, warnings);
                 }
             }
         }
@@ -946,16 +986,16 @@ public ClassNode stubClasses(ClassNode clazz, boolean enableRuntime) {
         if (clazz.methods != null) {
             for (MethodNode method : clazz.methods) {
                 type = Type.getMethodType(method.desc);
-                method.desc = stubClass(type).getDescriptor();
+                method.desc = stubClass(type, warnings).getDescriptor();
                 if (method.signature != null) {
-                    method.signature = transformSignature(method.signature);
+                    method.signature = transformSignature(method.signature, warnings);
                 }
                 if (method.localVariables != null) {
                     for (LocalVariableNode local : method.localVariables) {
                         type = Type.getType(local.desc);
-                        local.desc = stubClass(type).getDescriptor();
+                        local.desc = stubClass(type, warnings).getDescriptor();
                         if (local.signature != null) {
-                            local.signature = transformSignature(local.signature);
+                            local.signature = transformSignature(local.signature, warnings);
                         }
                     }
                 }
@@ -963,7 +1003,7 @@ public ClassNode stubClasses(ClassNode clazz, boolean enableRuntime) {
                     for (TryCatchBlockNode tryCatch : method.tryCatchBlocks) {
                         if (tryCatch.type != null) {
                             type = Type.getObjectType(tryCatch.type);
-                            tryCatch.type = stubClass(type).getInternalName();
+                            tryCatch.type = stubClass(type, warnings).getInternalName();
                         }
                     }
                 }
@@ -972,12 +1012,12 @@ public ClassNode stubClasses(ClassNode clazz, boolean enableRuntime) {
         return clazz;
     }
 
-    public String transformSignature(String signature) {
+    public String transformSignature(String signature, final Set<String> warnings) {
         SignatureReader reader = new SignatureReader(signature);
         SignatureWriter writer = new SignatureWriter() {
             @Override
             public void visitClassType(String name) {
-                super.visitClassType(stubClass(Type.getObjectType(name)).getInternalName());
+                super.visitClassType(stubClass(Type.getObjectType(name), warnings).getInternalName());
             }
 
             @Override
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/version/all/stub/J_L_Class.java b/src/main/java/xyz/wagyourtail/jvmdg/version/all/stub/J_L_Class.java
index 8f40c37e..2dd14d0e 100644
--- a/src/main/java/xyz/wagyourtail/jvmdg/version/all/stub/J_L_Class.java
+++ b/src/main/java/xyz/wagyourtail/jvmdg/version/all/stub/J_L_Class.java
@@ -109,10 +109,11 @@ public static Method[] getMethods(Class<?> clazz, int origVersion) {
         List<VersionProvider> versionProviders = ClassDowngrader.getCurrentVersionDowngrader().versionProviders(origVersion);
         List<Method> methods = new ArrayList<>(Arrays.asList(clazz.getMethods()));
         for (VersionProvider vp : versionProviders) {
+            Set<String> warnings = new HashSet<>();
             if (vp.classStubs.containsKey(target)) {
                 try {
                     ClassDowngrader downgrader = ClassDowngrader.getCurrentVersionDowngrader();
-                    List<Pair<Method, Stub>> targets = vp.getStubMapper(target, downgrader.isInterface(downgrader.target, target), getMethods, getSuperTypes).getStubTargets();
+                    List<Pair<Method, Stub>> targets = vp.getStubMapper(target, downgrader.isInterface(downgrader.target, target, warnings), getMethods, getSuperTypes).getStubTargets();
                     for (Pair<Method, Stub> t : targets) {
                         if (!methods.contains(t.getFirst())) {
                             methods.add(t.getFirst());
@@ -122,6 +123,11 @@ public static Method[] getMethods(Class<?> clazz, int origVersion) {
                     throw new RuntimeException(e);
                 }
             }
+            if (!warnings.isEmpty()) {
+                for (String warning : warnings) {
+                    System.err.println(warning);
+                }
+            }
         }
         return methods.toArray(new Method[0]);
     }
diff --git a/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java b/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java
index 338a96a8..9dfda874 100644
--- a/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java
+++ b/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java
@@ -7,6 +7,7 @@
 import xyz.wagyourtail.jvmdg.util.IOFunction;
 import xyz.wagyourtail.jvmdg.util.Lazy;
 import xyz.wagyourtail.jvmdg.util.Pair;
+import xyz.wagyourtail.jvmdg.version.Coverage;
 import xyz.wagyourtail.jvmdg.version.Modify;
 import xyz.wagyourtail.jvmdg.version.Stub;
 import xyz.wagyourtail.jvmdg.version.VersionProvider;
@@ -25,9 +26,12 @@ public class ClassMapping {
     private final Map<MemberNameAndDesc, Pair<Method, Stub>> methodStub = new HashMap<>();
     private final Map<MemberNameAndDesc, Pair<Method, Modify>> methodModify = new HashMap<>();
 
-    public ClassMapping(final Lazy<List<ClassMapping>> parents, final Type current, final boolean isInterface, final IOFunction<Type, Set<MemberNameAndDesc>> members, VersionProvider vp) {
+    private final Coverage coverage;
+
+    public ClassMapping(final Coverage coverage, final Lazy<List<ClassMapping>> parents, final Type current, final boolean isInterface, final IOFunction<Type, Set<MemberNameAndDesc>> members, VersionProvider vp) {
         this.parents = parents;
         this.current = current;
+        this.coverage = coverage;
         this.members = new Lazy<Map<MemberNameAndDesc, Pair<Boolean, Type>>>() {
 
             @Override
@@ -75,7 +79,18 @@ public void addModify(MemberNameAndDesc member, Method method, Modify modify) {
         methodModify.put(member, new Pair<>(method, modify));
     }
 
-    public void transform(MethodNode method, int index, ClassNode classNode, Set<ClassNode> extra, boolean runtimeAvailable) {
+    public void warnMember(MemberNameAndDesc member, Set<String> warnings) {
+        FullyQualifiedMemberNameAndDesc fqn = member.toFullyQualified(current);
+        String mod = coverage.checkMember(fqn);
+        if (mod != null) {
+            coverage.warnMember(fqn, warnings);
+        }
+        for (ClassMapping parent : parents.get()) {
+            parent.warnMember(member, warnings);
+        }
+    }
+
+    public void transform(MethodNode method, int index, ClassNode classNode, Set<ClassNode> extra, boolean runtimeAvailable, Set<String> warnings) {
         AbstractInsnNode insn = method.instructions.get(index);
         if (insn instanceof MethodInsnNode) {
             MethodInsnNode min = (MethodInsnNode) insn;
@@ -83,7 +98,7 @@ public void transform(MethodNode method, int index, ClassNode classNode, Set<Cla
             boolean isStatic = insn.getOpcode() == Opcodes.INVOKESTATIC;
             boolean isSpecial = insn.getOpcode() == Opcodes.INVOKESPECIAL;
 
-            Pair<Method, Stub> newMin = getStubFor(member, isStatic, runtimeAvailable, isSpecial);
+            Pair<Method, Stub> newMin = getStubFor(member, isStatic, runtimeAvailable, isSpecial, warnings);
             Type returnType = Type.getReturnType(min.desc);
             if (newMin != null) {
                 // handled specially, by inserting a call to the stub in the implementation if it's missing an implementation.
@@ -139,11 +154,12 @@ public void transform(MethodNode method, int index, ClassNode classNode, Set<Cla
                 method.instructions.remove(min);
                 return;
             }
-            Pair<Method, Modify> m = methodModify.get(member);
+            Pair<Method, Modify> m = getModifyFor(member, isStatic, warnings);
             if (m != null) {
                 try {
                     List<Object> modifyArgs = Arrays.asList(method, index, classNode, extra);
                     m.getFirst().invoke(null, modifyArgs.subList(0, m.getFirst().getParameterTypes().length).toArray());
+                    return;
                 } catch (Throwable e) {
                     throw new RuntimeException(e);
                 }
@@ -160,13 +176,14 @@ public void transform(MethodNode method, int index, ClassNode classNode, Set<Cla
                 } catch (Throwable e) {
                     throw new RuntimeException(e);
                 }
+                return;
             }
         }
     }
 
-    public Pair<Method, Stub> getParentStubFor(MemberNameAndDesc member, boolean runtimeAvailable, boolean special) {
+    public Pair<Method, Stub> getParentStubFor(MemberNameAndDesc member, boolean runtimeAvailable, boolean special, Set<String> warnings) {
         for (ClassMapping parent : parents.get()) {
-            Pair<Method, Stub> node = parent.getStubFor(member, false, runtimeAvailable, special);
+            Pair<Method, Stub> node = parent.getStubFor(member, false, runtimeAvailable, special, warnings);
             if (node != null) {
                 return node;
             }
@@ -174,7 +191,7 @@ public Pair<Method, Stub> getParentStubFor(MemberNameAndDesc member, boolean run
         return null;
     }
 
-    public Pair<Method, Stub> getStubFor(MemberNameAndDesc member, boolean invoke_static, boolean runtimeAvailable, boolean special) {
+    public Pair<Method, Stub> getStubFor(MemberNameAndDesc member, boolean invoke_static, boolean runtimeAvailable, boolean special, Set<String> warnings) {
         try {
             Pair<Method, Stub> pair = methodStub.get(member);
             if (pair == null) {
@@ -186,13 +203,14 @@ public Pair<Method, Stub> getStubFor(MemberNameAndDesc member, boolean invoke_st
 //                        }
                         return null;
                     }
-                    return getParentStubFor(member, runtimeAvailable, special);
+                    return getParentStubFor(member, runtimeAvailable, special, warnings);
                 }
+                warnMember(member, warnings);
                 return null;
             }
             Method m = pair.getFirst();
             if (!runtimeAvailable && pair.getSecond().requiresRuntime()) {
-                System.err.println("WARNING: " + m + " requires runtime transformation but runtime is not available...");
+                warnings.add(m + " requires runtime transformation but runtime is not available...");
             }
             if (special && pair.getSecond().noSpecial()) {
                 return null;
@@ -207,9 +225,9 @@ public Pair<Method, Stub> getStubFor(MemberNameAndDesc member, boolean invoke_st
         }
     }
 
-    public Pair<Method, Modify> getParentModifyFor(MemberNameAndDesc member) {
+    public Pair<Method, Modify> getParentModifyFor(MemberNameAndDesc member, Set<String> warnings) {
         for (ClassMapping parent : parents.get()) {
-            Pair<Method, Modify> node = parent.getModifyFor(member, false);
+            Pair<Method, Modify> node = parent.getModifyFor(member, false, warnings);
             if (node != null) {
                 return node;
             }
@@ -217,7 +235,7 @@ public Pair<Method, Modify> getParentModifyFor(MemberNameAndDesc member) {
         return null;
     }
 
-    public Pair<Method, Modify> getModifyFor(MemberNameAndDesc member, boolean invoke_static) {
+    public Pair<Method, Modify> getModifyFor(MemberNameAndDesc member, boolean invoke_static, Set<String> warnings) {
         try {
             Pair<Method, Modify> pair = methodModify.get(member);
             if (pair == null) {
@@ -226,8 +244,9 @@ public Pair<Method, Modify> getModifyFor(MemberNameAndDesc member, boolean invok
                     if (members != null && members.containsKey(member)) {
                         return null;
                     }
-                    return getParentModifyFor(member);
+                    return getParentModifyFor(member, warnings);
                 }
+                warnMember(member, warnings);
                 return null;
             }
             Method m = pair.getFirst();
diff --git a/src/shared/java/xyz/wagyourtail/jvmdg/util/Utils.java b/src/shared/java/xyz/wagyourtail/jvmdg/util/Utils.java
index a586d6a6..f7aa913d 100644
--- a/src/shared/java/xyz/wagyourtail/jvmdg/util/Utils.java
+++ b/src/shared/java/xyz/wagyourtail/jvmdg/util/Utils.java
@@ -1,5 +1,6 @@
 package xyz.wagyourtail.jvmdg.util;
 
+import org.objectweb.asm.Opcodes;
 import sun.misc.Unsafe;
 
 import java.io.ByteArrayOutputStream;
@@ -71,6 +72,11 @@ public static int getCurrentClassVersion() {
         throw new UnsupportedOperationException("Unable to determine current class version");
     }
 
+    public static int classVersionToMajorVersion(int version) {
+        if (version == Opcodes.V1_1) return 1;
+        else return version - Opcodes.V1_2 + 2;
+    }
+
     public static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
         throw (T) t;
     }
diff --git a/src/test/java/xyz/wagyourtail/jvmdg/internal/JvmDowngraderTest.java b/src/test/java/xyz/wagyourtail/jvmdg/internal/JvmDowngraderTest.java
index 8e444fdd..3c0b6866 100644
--- a/src/test/java/xyz/wagyourtail/jvmdg/internal/JvmDowngraderTest.java
+++ b/src/test/java/xyz/wagyourtail/jvmdg/internal/JvmDowngraderTest.java
@@ -170,7 +170,7 @@ private void testDowngrade(String mainClass, boolean eq) throws Exception {
                 new String[]{
                         "-a",
                         javaApi.toString(),
-                        "--quiet",
+//                        "--quiet",
                         "bootstrap",
                         "--classpath",
                         original.toString(),