docs: private ScanResult performScan(final List<ClasspathElement> finalClasspathEltOrder,
              final List<String> finalClasspathEltOrderStrs,
              final ClassLoader[] classLoaderOrderRespectingParentDelegation)
              throws InterruptedException, ExecutionException {
          // Mask classfiles (remove any classfile resources that are shadowed by an earlier definition
          // of the same class)
          if (scanSpec.enableClassInfo) {
              maskClassfiles(finalClasspathEltOrder,
                      topLevelLog == null ? null : topLevelLog.log("Masking classfiles"));
          }
  
          // Merge the file-to-timestamp maps across all classpath elements
          final Map<File, Long> fileToLastModified = new HashMap<>();
          for (final ClasspathElement classpathElement : finalClasspathEltOrder) {
              fileToLastModified.putAll(classpathElement.fileToLastModified);
          }
  
          // Scan classfiles, if scanSpec.enableClassInfo is true
          final Map<String, ClassInfo> classNameToClassInfo = new HashMap<>();
          final Map<String, PackageInfo> packageNameToPackageInfo = new HashMap<>();
          final Map<String, ModuleInfo> moduleNameToModuleInfo = new HashMap<>();
          if (scanSpec.enableClassInfo) {
              // Get whitelisted classfile order
              final List<ClassfileScanWorkUnit> classfileScanWorkItems = new ArrayList<>();
              final Set<String> classNamesScheduledForScanning = Collections
                      .newSetFromMap(new ConcurrentHashMap<String, Boolean>());
              for (final ClasspathElement classpathElement : finalClasspathEltOrder) {
                  // Get classfile scan order across all classpath elements
                  for (final Resource resource : classpathElement.whitelistedClassfileResources) {
                      classfileScanWorkItems
                              .add(new ClassfileScanWorkUnit(classpathElement, resource, /* isExternal = */ false));
                      // Pre-seed scanned class names with all whitelisted classes (since these will
                      // be scanned for sure)
                      classNamesScheduledForScanning.add(JarUtils.classfilePathToClassName(resource.getPath()));
                  }
              }
  
              // Scan classfiles in parallel.
              final Queue<Classfile> scannedClassfiles = new ConcurrentLinkedQueue<>();
              final ConcurrentHashMap<String, String> stringInternMap = new ConcurrentHashMap<>();
              processWorkUnits(classfileScanWorkItems,
                      topLevelLog == null ? null : topLevelLog.log("Scanning classfiles"),
                      new ClassfileScannerWorkUnitProcessor(scanSpec, finalClasspathEltOrder,
                              classNamesScheduledForScanning, scannedClassfiles, stringInternMap));
              stringInternMap.clear();
  
              // Link the Classfile objects to produce ClassInfo objects. This needs to be done from a single thread.
              final LogNode linkLog = topLevelLog == null ? null : topLevelLog.log("Linking related classfiles");
              for (final Classfile c : scannedClassfiles) {
                  c.link(classNameToClassInfo, packageNameToPackageInfo, moduleNameToModuleInfo);
              }
  
              // Uncomment the following code to create placeholder external classes for any classes
              // referenced in type descriptors or type signatures, so that a ClassInfo object can be
              // obtained for those class references. This will cause all type descriptors and type
              // signatures to be parsed, and class names extracted from them. This will add some
              // overhead to the scanning time, and the only benefit is that
              // ClassRefTypeSignature.getClassInfo() and AnnotationClassRef.getClassInfo() will never
              // return null, since all external classes found in annotation class refs will have a
              // placeholder ClassInfo object created for them. This is obscure enough that it is
              // probably not worth slowing down scanning for all other usecases, by forcibly parsing
              // all type descriptors and type signatures before returning the ScanResult.
              // With this code commented out, type signatures and type descriptors are only parsed
              // lazily, on demand.
  
              //    final Set<String> referencedClassNames = new HashSet<>();
              //    for (final ClassInfo classInfo : classNameToClassInfo.values()) {
              //        classInfo.findReferencedClassNames(referencedClassNames);
              //    }
              //    for (final String referencedClass : referencedClassNames) {
              //        ClassInfo.getOrCreateClassInfo(referencedClass, /* modifiers = */ 0, scanSpec,
              //                classNameToClassInfo);
              //    }
  
              if (linkLog != null) {
                  linkLog.addElapsedTime();
              }
          } else {
              if (topLevelLog != null) {
                  topLevelLog.log("Classfile scanning is disabled");
              }
          }
  
          // Return a new ScanResult
          return new ScanResult(scanSpec, finalClasspathEltOrder, finalClasspathEltOrderStrs,
                  classLoaderOrderRespectingParentDelegation, classNameToClassInfo, packageNameToPackageInfo,
                  moduleNameToModuleInfo, fileToLastModified, nestedJarHandler, topLevelLog);
      }