Merge branch '317979-generate-module-info-classes-without-bnd-plugin' into 'master'

Generate the module info classes without Bnd. Delete the Bnd plugin.

Closes #317979

See merge request asm/asm!355
diff --git a/build.gradle b/build.gradle
index ce6a73b..26cab3a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,6 +60,10 @@
   test { useJUnitPlatform() }
   ext.provides = []  // The provided java packages, e.g. ['org.objectweb.asm']
   ext.requires = []  // The required Gradle projects, e.g. [':asm-test']
+  ext.transitiveRequires = { ->
+    return requires.collect{p -> 
+        project(p).transitiveRequires().plus(project(p).provides[0])}.flatten()
+  }
   ext.depends = []   // The external dependencies, e.g. ['junit:junit:4.12']
   // Some external dependencies (such as Jacoco) depend transitively on ASM, and
   // without this rule Gradle can mix ASM jars of different versions (e.g.
@@ -85,7 +89,7 @@
 project(':asm-commons') {
   description = "Usefull class adapters based on ${parent.description}"
   provides = ['org.objectweb.asm.commons']
-  requires = [':asm', ':asm-tree', ':asm-analysis']
+  requires = [':asm', ':asm-tree']
   dependencies { testImplementation project(':asm-util') }
 }
 
@@ -159,16 +163,11 @@
   description = "Tools used to build ${parent.description}"
 }
 
-project(':tools:bnd-module-plugin') {
-  description = "bnd plugin to build moduleinfo with ${rootProject.description}"
-  // TODO: this compiles asm twice (here and in :asm), find a way to avoid this.
-  sourceSets.main.java.srcDirs += project(':asm').sourceSets.main.java.srcDirs
-  depends = ['biz.aQute.bnd:biz.aQute.bnd:6.2.0']
-}
-
 project(':tools:retrofitter') {
   description = "JDK 1.5 class retrofitter based on ${rootProject.description}"
-  // TODO: this compiles asm thrice (here, above and in :asm).
+  sourceCompatibility = '1.9'
+  targetCompatibility = '1.9'
+  // TODO: this compiles asm twice (here and in :asm).
   sourceSets.main.java.srcDirs += project(':asm').sourceSets.main.java.srcDirs
 }
 
@@ -215,7 +214,7 @@
 
 // Configure the projects with a non-empty 'provides' property. They must be
 // checked for code coverage and backward compatibility, retrofited to Java 1.5,
-// and packaged with biz.aQute.bnd.
+// and packaged with generated module-info classes.
 configure(subprojects.findAll { it.provides }) {
   // Code coverage configuration.
   jacoco.toolVersion = '0.8.8'
@@ -229,7 +228,8 @@
   }
   check.dependsOn jacocoTestCoverageVerification
 
-  // Retrofit the code to Java 1.5, in-place, in compileJava.doLast.
+  // Retrofit the code in-place to Java 1.5 and generate a module-info class
+  // from the code content, in compileJava.doLast.
   if (name != 'asm-test') {
     compileJava.dependsOn ':tools:retrofitter:classes'
     compileJava.doLast {
@@ -237,7 +237,9 @@
       def loader = new URLClassLoader(path.collect {f -> f.toURL()} as URL[])
       def retrofitter =
           loader.loadClass('org.objectweb.asm.tools.Retrofitter').newInstance()
-      retrofitter.retrofit sourceSets.main.output.classesDirs.singleFile
+      def classes = sourceSets.main.output.classesDirs.singleFile
+      retrofitter.retrofit(classes, "${version}")
+      retrofitter.verify(classes, "${version}", provides, transitiveRequires())
     }
   }
 
@@ -279,18 +281,13 @@
     }
   }
 
-  // Apply the biz.aQute.bnd plugin to package the project as an OSGi bundle, 
-  // with a custom plugin to generate and include a module-info class. Exclude
-  // the asm-test project (the DefaultPackage class prevents it from being a
-  // proper bundle).
+  // Apply the biz.aQute.bnd plugin to package the project as an OSGi bundle.
+  // Exclude the asm-test project (the DefaultPackage class prevents it from
+  // being a proper bundle).
   if (name != 'asm-test') {
     apply plugin: 'biz.aQute.bnd.builder'
-    jar.dependsOn ':tools:bnd-module-plugin:jar'
     jar.manifest.attributes(
       '-classpath': sourceSets.main.output.classesDirs.asPath,
-      '-plugin': 'org.objectweb.asm.tools.ModuleInfoBndPlugin;',
-      '-pluginpath': 
-          project(':tools:bnd-module-plugin').jar.outputs.files.singleFile,
       '-removeheaders': 'Bnd-LastModified,Build-By,Created-By,Include-Resource,\
           Require-Capability,Tool',
       'Bundle-License': 'BSD-3-Clause;link=https://asm.ow2.io/LICENSE.txt',
diff --git a/settings.gradle b/settings.gradle
index a849bc1..bed9a5d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -36,6 +36,5 @@
   'asm-tree',
   'asm-util',
   'benchmarks',
-  'tools:bnd-module-plugin',
   'tools:retrofitter')
 
diff --git a/tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java b/tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java
deleted file mode 100644
index ee91bdc..0000000
--- a/tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java
+++ /dev/null
@@ -1,99 +0,0 @@
-// ASM: a very small and fast Java bytecode manipulation framework
-// Copyright (c) 2000-2011 INRIA, France Telecom
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions
-// are met:
-// 1. Redistributions of source code must retain the above copyright
-//    notice, this list of conditions and the following disclaimer.
-// 2. Redistributions in binary form must reproduce the above copyright
-//    notice, this list of conditions and the following disclaimer in the
-//    documentation and/or other materials provided with the distribution.
-// 3. Neither the name of the copyright holders nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
-// THE POSSIBILITY OF SUCH DAMAGE.
-package org.objectweb.asm.tools;
-
-import aQute.bnd.header.Attrs;
-import aQute.bnd.header.Parameters;
-import aQute.bnd.osgi.Analyzer;
-import aQute.bnd.osgi.Constants;
-import aQute.bnd.osgi.EmbeddedResource;
-import aQute.bnd.service.AnalyzerPlugin;
-import org.objectweb.asm.ClassWriter;
-import org.objectweb.asm.ModuleVisitor;
-import org.objectweb.asm.Opcodes;
-
-/**
- * An biz.aQute.bnd plugin to generate a module-info class from the name, version, requires and
- * export properties of the bundle.
- *
- * @author Remi Forax
- */
-public class ModuleInfoBndPlugin implements AnalyzerPlugin {
-  private static final String MODULE_NAME = "Module-Name";
-  private static final String MODULE_VERSION = "Module-Version";
-  private static final String MODULE_REQUIRES = "Module-Requires";
-  private static final String MODULE_EXPORTS = "Module-Exports";
-
-  @Override
-  public boolean analyzeJar(final Analyzer analyzer) throws Exception {
-    ClassWriter classWriter = new ClassWriter(0);
-    classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
-    String moduleName =
-        analyzer.getProperty(MODULE_NAME, analyzer.getProperty(Constants.BUNDLE_SYMBOLICNAME));
-    String moduleVersion =
-        analyzer.getProperty(MODULE_VERSION, analyzer.getProperty(Constants.BUNDLE_VERSION));
-    ModuleVisitor moduleVisitor =
-        classWriter.visitModule(moduleName, Opcodes.ACC_OPEN, moduleVersion);
-
-    String requireModules = analyzer.getProperty(MODULE_REQUIRES);
-    if (requireModules != null) {
-      Parameters requireParams = analyzer.parseHeader(requireModules);
-      for (String requireName : requireParams.keySet()) {
-        Attrs attrs = requireParams.get(requireName);
-        boolean isTransitive = attrs.containsKey("transitive");
-        boolean isStatic = attrs.containsKey("static");
-        moduleVisitor.visitRequire(
-            requireName,
-            (isTransitive ? Opcodes.ACC_TRANSITIVE : 0) | (isStatic ? Opcodes.ACC_STATIC_PHASE : 0),
-            null);
-      }
-    }
-    moduleVisitor.visitRequire("java.base", Opcodes.ACC_MANDATED, null);
-
-    String exportPackages =
-        analyzer.getProperty(MODULE_EXPORTS, analyzer.getProperty(Constants.EXPORT_PACKAGE));
-    if (exportPackages != null) {
-      Parameters exportParams = analyzer.parseHeader(exportPackages);
-      for (String packageName : exportParams.keySet()) {
-        if (packageName.endsWith("*")) {
-          throw new IllegalStateException("Unsupported wildcard packages " + packageName);
-        }
-        moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
-      }
-    }
-    moduleVisitor.visitEnd();
-    classWriter.visitEnd();
-
-    analyzer
-        .getJar()
-        .putResource(
-            "module-info.class",
-            new EmbeddedResource(classWriter.toByteArray(), System.currentTimeMillis()));
-    return false;
-  }
-}
diff --git a/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java b/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java
index 2d017ed..b3d51e6 100644
--- a/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java
+++ b/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java
@@ -27,22 +27,33 @@
 // THE POSSIBILITY OF SUCH DAMAGE.
 package org.objectweb.asm.tools;
 
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toSet;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.LineNumberReader;
-import java.io.OutputStream;
+import java.lang.module.ModuleDescriptor;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
 import java.util.zip.GZIPInputStream;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.ModuleVisitor;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
@@ -57,6 +68,12 @@
  */
 public class Retrofitter {
 
+  /** The name of the module-info file. */
+  private static final String MODULE_INFO = "module-info.class";
+
+  /** The name of the java.base module. */
+  private static final String JAVA_BASE_MODULE = "java.base";
+
   /**
    * The fields and methods of the JDK 1.5 API. Each string has the form
    * "<owner><name><descriptor>".
@@ -68,12 +85,158 @@
    */
   private final HashMap<String, String> jdkHierarchy = new HashMap<>();
 
+  /** The internal names of the packages exported by the retrofitted classes. */
+  private final HashSet<String> exports = new HashSet<>();
+
+  /** The internal names of the packages imported by the retrofitted classes. */
+  private final HashSet<String> imports = new HashSet<>();
+
   /**
-   * Constructs a new {@link Retrofitter}.
+   * Transforms the class files in the given directory, in place, in order to make them compatible
+   * with the JDK 1.5. Also generates a module-info class in this directory, with the given module
+   * version.
    *
-   * @throws IOException if the JDK API description file can't be read.
+   * @param args a directory containing compiled classes and the ASM release version.
+   * @throws IOException if a file can't be read or written.
    */
-  public Retrofitter() throws IOException {
+  public static void main(final String[] args) throws IOException {
+    if (args.length == 2) {
+      new Retrofitter().retrofit(new File(args[0]), args[1]);
+    } else {
+      System.err.println("Usage: Retrofitter <classes directory> <ASM release version>"); // NOPMD
+    }
+  }
+
+  /**
+   * Transforms the class files in the given directory, in place, in order to make them compatible
+   * with the JDK 1.5. Also generates a module-info class in this directory, with the given module
+   * version.
+   *
+   * @param classesDir a directory containing compiled classes.
+   * @param version the module-info version.
+   * @throws IOException if a file can't be read or written.
+   */
+  public void retrofit(final File classesDir, final String version) throws IOException {
+    for (File classFile : getAllClasses(classesDir, new ArrayList<File>())) {
+      ClassReader classReader = new ClassReader(Files.newInputStream(classFile.toPath()));
+      ClassWriter classWriter = new ClassWriter(0);
+      classReader.accept(new ClassRetrofitter(classWriter), ClassReader.SKIP_FRAMES);
+      Files.write(classFile.toPath(), classWriter.toByteArray());
+    }
+    generateModuleInfoClass(classesDir, version);
+  }
+
+  /**
+   * Verify that the class files in the given directory only use JDK 1.5 APIs, and that a
+   * module-info class is present with the expected content.
+   *
+   * @param classesDir a directory containing compiled classes.
+   * @param expectedVersion the expected module-info version.
+   * @param expectedExports the expected module-info exported packages.
+   * @param expectedRequires the expected module-info required modules.
+   * @throws IOException if a file can't be read.
+   * @throws IllegalArgumentException if the module-info class does not have the expected content.
+   */
+  public void verify(
+      final File classesDir,
+      final String expectedVersion,
+      final List<String> expectedExports,
+      final List<String> expectedRequires)
+      throws IOException {
+    if (jdkApi.isEmpty()) {
+      readJdkApi();
+    }
+    for (File classFile : getAllClasses(classesDir, new ArrayList<File>())) {
+      if (!classFile.getName().equals(MODULE_INFO)) {
+        new ClassReader(Files.newInputStream(classFile.toPath())).accept(new ClassVerifier(), 0);
+      }
+    }
+    verifyModuleInfoClass(
+        classesDir,
+        expectedVersion,
+        new HashSet<String>(expectedExports),
+        Stream.concat(expectedRequires.stream(), Stream.of(JAVA_BASE_MODULE)).collect(toSet()));
+  }
+
+  private List<File> getAllClasses(final File file, final List<File> allClasses)
+      throws IOException {
+    if (file.isDirectory()) {
+      File[] children = file.listFiles();
+      if (children == null) {
+        throw new IOException("Unable to read files of " + file);
+      }
+      for (File child : children) {
+        getAllClasses(child, allClasses);
+      }
+    } else if (file.getName().endsWith(".class")) {
+      allClasses.add(file);
+    }
+    return allClasses;
+  }
+
+  private void generateModuleInfoClass(final File dstDir, final String version) throws IOException {
+    ClassWriter classWriter = new ClassWriter(0);
+    classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
+    ArrayList<String> moduleNames = new ArrayList<>();
+    for (String exportName : exports) {
+      if (isAsmModule(exportName)) {
+        moduleNames.add(exportName);
+      }
+    }
+    if (moduleNames.size() != 1) {
+      throw new IllegalArgumentException("Module name can't be infered from classes");
+    }
+    ModuleVisitor moduleVisitor =
+        classWriter.visitModule(moduleNames.get(0), Opcodes.ACC_OPEN, version);
+
+    for (String importName : imports) {
+      if (isAsmModule(importName) && !exports.contains(importName)) {
+        moduleVisitor.visitRequire(importName.replace('/', '.'), Opcodes.ACC_TRANSITIVE, null);
+      }
+    }
+    moduleVisitor.visitRequire(JAVA_BASE_MODULE, Opcodes.ACC_MANDATED, null);
+
+    for (String exportName : exports) {
+      moduleVisitor.visitExport(exportName, 0);
+    }
+    moduleVisitor.visitEnd();
+    classWriter.visitEnd();
+    Files.write(Path.of(dstDir.getAbsolutePath(), MODULE_INFO), classWriter.toByteArray());
+  }
+
+  private void verifyModuleInfoClass(
+      final File dstDir,
+      final String expectedVersion,
+      final Set<String> expectedExports,
+      final Set<String> expectedRequires)
+      throws IOException {
+    ModuleDescriptor module =
+        ModuleDescriptor.read(Files.newInputStream(Path.of(dstDir.getAbsolutePath(), MODULE_INFO)));
+    String version = module.version().map(ModuleDescriptor.Version::toString).orElse("");
+    if (!version.equals(expectedVersion)) {
+      throw new IllegalArgumentException(
+          format("Wrong module-info version '%s' (expected '%s')", version, expectedVersion));
+    }
+    Set<String> exports =
+        module.exports().stream().map(ModuleDescriptor.Exports::source).collect(toSet());
+    if (!exports.equals(expectedExports)) {
+      throw new IllegalArgumentException(
+          format("Wrong module-info exports %s (expected %s)", exports, expectedExports));
+    }
+    Set<String> requires =
+        module.requires().stream().map(ModuleDescriptor.Requires::name).collect(toSet());
+    if (!requires.equals(expectedRequires)) {
+      throw new IllegalArgumentException(
+          format("Wrong module-info requires %s (expected %s)", requires, expectedRequires));
+    }
+  }
+
+  private static boolean isAsmModule(final String packageName) {
+    return packageName.startsWith("org/objectweb/asm")
+        && !packageName.equals("org/objectweb/asm/signature");
+  }
+
+  private void readJdkApi() throws IOException {
     try (InputStream inputStream =
             new GZIPInputStream(
                 Retrofitter.class.getClassLoader().getResourceAsStream("jdk1.5.0.12.txt.gz"));
@@ -97,56 +260,8 @@
     }
   }
 
-  /**
-   * Transforms the source class file, or if it is a directory, its files (recursively), in place,
-   * in order to make them compatible with the JDK 1.5.
-   *
-   * @param src source file or directory.
-   * @throws IOException if the source files can't be read or written.
-   */
-  public void retrofit(final File src) throws IOException {
-    retrofit(src, null);
-  }
-
-  /**
-   * Transforms the source class file, or if it is a directory, its files (recursively), either in
-   * place or into the destination file or directory, in order to make them compatible with the JDK
-   * 1.5.
-   *
-   * @param src source file or directory.
-   * @param dst optional destination file or directory.
-   * @throws IOException if the source or destination file can't be read or written.
-   */
-  public void retrofit(final File src, final File dst) throws IOException {
-    if (src.isDirectory()) {
-      File[] files = src.listFiles();
-      if (files == null) {
-        throw new IOException("Unable to read files of " + src);
-      }
-      for (File file : files) {
-        retrofit(file, dst == null ? null : new File(dst, file.getName()));
-      }
-    } else if (src.getName().endsWith(".class")) {
-      if (dst == null || !dst.exists() || dst.lastModified() < src.lastModified()) {
-        ClassReader classReader = new ClassReader(Files.newInputStream(src.toPath()));
-        ClassWriter classWriter = new ClassWriter(0);
-        ClassVerifier classVerifier = new ClassVerifier(classWriter);
-        ClassRetrofitter classRetrofitter = new ClassRetrofitter(classVerifier);
-        classReader.accept(classRetrofitter, ClassReader.SKIP_FRAMES);
-
-        if (dst != null && !dst.getParentFile().exists() && !dst.getParentFile().mkdirs()) {
-          throw new IOException("Cannot create directory " + dst.getParentFile());
-        }
-        try (OutputStream outputStream =
-            Files.newOutputStream((dst == null ? src : dst).toPath())) {
-          outputStream.write(classWriter.toByteArray());
-        }
-      }
-    }
-  }
-
   /** A ClassVisitor that retrofits classes to 1.5 version. */
-  static class ClassRetrofitter extends ClassVisitor {
+  class ClassRetrofitter extends ClassVisitor {
 
     public ClassRetrofitter(final ClassVisitor classVisitor) {
       super(/* latest api =*/ Opcodes.ASM8, classVisitor);
@@ -160,26 +275,47 @@
         final String signature,
         final String superName,
         final String[] interfaces) {
+      addPackageReferences(Type.getObjectType(name), /* export = */ true);
       super.visit(Opcodes.V1_5, access, name, signature, superName, interfaces);
     }
 
     @Override
+    public FieldVisitor visitField(
+        final int access,
+        final String name,
+        final String descriptor,
+        final String signature,
+        final Object value) {
+      addPackageReferences(Type.getType(descriptor), /* export = */ false);
+      return super.visitField(access, name, descriptor, signature, value);
+    }
+
+    @Override
     public MethodVisitor visitMethod(
         final int access,
         final String name,
         final String descriptor,
         final String signature,
         final String[] exceptions) {
+      addPackageReferences(Type.getType(descriptor), /* export = */ false);
       return new MethodVisitor(
           api, super.visitMethod(access, name, descriptor, signature, exceptions)) {
 
         @Override
+        public void visitFieldInsn(
+            final int opcode, final String owner, final String name, final String descriptor) {
+          addPackageReferences(Type.getType(descriptor), /* export = */ false);
+          super.visitFieldInsn(opcode, owner, name, descriptor);
+        }
+
+        @Override
         public void visitMethodInsn(
             final int opcode,
             final String owner,
             final String name,
             final String descriptor,
             final boolean isInterface) {
+          addPackageReferences(Type.getType(descriptor), /* export = */ false);
           // Remove the addSuppressed() method calls generated for try-with-resources statements.
           // This method is not defined in JDK1.5.
           if (owner.equals("java/lang/Throwable")
@@ -190,8 +326,52 @@
             super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
           }
         }
+
+        @Override
+        public void visitTypeInsn(final int opcode, final String type) {
+          addPackageReferences(Type.getObjectType(type), /* export = */ false);
+          super.visitTypeInsn(opcode, type);
+        }
+
+        @Override
+        public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) {
+          addPackageReferences(Type.getType(descriptor), /* export = */ false);
+          super.visitMultiANewArrayInsn(descriptor, numDimensions);
+        }
+
+        @Override
+        public void visitTryCatchBlock(
+            final Label start, final Label end, final Label handler, final String type) {
+          if (type != null) {
+            addPackageReferences(Type.getObjectType(type), /* export = */ false);
+          }
+          super.visitTryCatchBlock(start, end, handler, type);
+        }
       };
     }
+
+    private void addPackageReferences(final Type type, final boolean export) {
+      switch (type.getSort()) {
+        case Type.ARRAY:
+          addPackageReferences(type.getElementType(), export);
+          break;
+        case Type.METHOD:
+          for (Type argumentType : type.getArgumentTypes()) {
+            addPackageReferences(argumentType, export);
+          }
+          addPackageReferences(type.getReturnType(), export);
+          break;
+        case Type.OBJECT:
+          String internalName = type.getInternalName();
+          int lastSlashIndex = internalName.lastIndexOf('/');
+          if (lastSlashIndex != -1) {
+            (export ? exports : imports).add(internalName.substring(0, lastSlashIndex));
+          }
+          break;
+        default:
+          break;
+      }
+    }
   }
 
   /**
@@ -199,18 +379,18 @@
    */
   class ClassVerifier extends ClassVisitor {
 
-    /** The name of the visited class. */
+    /** The internal name of the visited class. */
     String className;
 
     /** The name of the currently visited method. */
     String currentMethodName;
 
-    public ClassVerifier(final ClassVisitor classVisitor) {
+    public ClassVerifier() {
       // Make sure use we don't use Java 9 or higher classfile features.
       // We also want to make sure we don't use Java 6, 7 or 8 classfile
       // features (invokedynamic), but this can't be done in the same way.
       // Instead, we use manual checks below.
-      super(Opcodes.ASM4, classVisitor);
+      super(Opcodes.ASM4, null);
     }
 
     @Override
@@ -222,10 +402,9 @@
         final String superName,
         final String[] interfaces) {
       if ((version & 0xFFFF) > Opcodes.V1_5) {
-        throw new IllegalArgumentException("ERROR: " + name + " version is newer than 1.5");
+        throw new IllegalArgumentException(format("ERROR: %d version is newer than 1.5", version));
       }
       className = name;
-      super.visit(version, access, name, signature, superName, interfaces);
     }
 
     @Override
@@ -243,7 +422,6 @@
         public void visitFieldInsn(
             final int opcode, final String owner, final String name, final String descriptor) {
           check(owner, name);
-          super.visitFieldInsn(opcode, owner, name, descriptor);
         }
 
         @Override
@@ -254,7 +432,6 @@
             final String descriptor,
             final boolean isInterface) {
           check(owner, name + descriptor);
-          super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
         }
 
         @Override
@@ -263,21 +440,16 @@
             int sort = ((Type) value).getSort();
             if (sort == Type.METHOD) {
               throw new IllegalArgumentException(
-                  "ERROR: ldc with a MethodType called in "
-                      + className
-                      + ' '
-                      + currentMethodName
-                      + " is not available in JDK 1.5");
+                  format(
+                      "ERROR: ldc with a MethodType called in %s %s is not available in JDK 1.5",
+                      className, currentMethodName));
             }
           } else if (value instanceof Handle) {
             throw new IllegalArgumentException(
-                "ERROR: ldc with a MethodHandle called in "
-                    + className
-                    + ' '
-                    + currentMethodName
-                    + " is not available in JDK 1.5");
+                format(
+                    "ERROR: ldc with a MethodHandle called in %s %s is not available in JDK 1.5",
+                    className, currentMethodName));
           }
-          super.visitLdcInsn(value);
         }
 
         @Override
@@ -287,11 +459,9 @@
             final Handle bootstrapMethodHandle,
             final Object... bootstrapMethodArguments) {
           throw new IllegalArgumentException(
-              "ERROR: invokedynamic called in "
-                  + className
-                  + ' '
-                  + currentMethodName
-                  + " is not available in JDK 1.5");
+              format(
+                  "ERROR: invokedynamic called in %s %s is not available in JDK 1.5",
+                  className, currentMethodName));
         }
       };
     }
@@ -302,7 +472,7 @@
      * @param owner A class name.
      * @param member A field name or a method name and descriptor.
      */
-    void check(final String owner, final String member) {
+    private void check(final String owner, final String member) {
       if (owner.startsWith("java/")) {
         String currentOwner = owner;
         while (currentOwner != null) {
@@ -312,15 +482,9 @@
           currentOwner = jdkHierarchy.get(currentOwner);
         }
         throw new IllegalArgumentException(
-            "ERROR: "
-                + owner
-                + ' '
-                + member
-                + " called in "
-                + className
-                + ' '
-                + currentMethodName
-                + " is not defined in the JDK 1.5 API");
+            format(
+                "ERROR: %s %s called in %s %s is not defined in the JDK 1.5 API",
+                owner, member, className, currentMethodName));
       }
     }
   }