All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opalj.br.analyses.Project.scala Maven / Gradle / Ivy

The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package br
package analyses

import scala.annotation.switch
import scala.annotation.tailrec
import java.io.File
import java.lang.ref.SoftReference
import java.net.URL
import java.util.Arrays.{sort => sortArray}
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReferenceArray
import scala.collection.Map
import scala.collection.Set
import scala.collection.immutable
import scala.collection.mutable
import scala.collection.mutable.AnyRefMap
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.Buffer
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import org.opalj.log.Error
import org.opalj.log.GlobalLogContext
import org.opalj.log.LogContext
import org.opalj.log.OPALLogger
import org.opalj.log.OPALLogger.error
import org.opalj.log.OPALLogger.info
import org.opalj.log.StandardLogContext
import org.opalj.util.PerformanceEvaluation.time
import org.opalj.collection.immutable.UIDSet
import org.opalj.concurrent.ConcurrentExceptions
import org.opalj.concurrent.SequentialTasks
import org.opalj.concurrent.Tasks
import org.opalj.concurrent.defaultIsInterrupted
import org.opalj.concurrent.NumberOfThreadsForCPUBoundTasks
import org.opalj.concurrent.parForeachArrayElement
import org.opalj.br.instructions.Instruction
import org.opalj.br.instructions.INVOKESPECIAL
import org.opalj.br.instructions.INVOKESTATIC
import org.opalj.br.instructions.NEW
import org.opalj.br.instructions.NonVirtualMethodInvocationInstruction
import org.opalj.br.reader.BytecodeInstructionsCache
import org.opalj.br.reader.Java17FrameworkWithDynamicRewritingAndCaching
import org.opalj.br.reader.Java17LibraryFramework

import scala.collection.immutable.ArraySeq

/**
 * Primary abstraction of a Java project; i.e., a set of classes that constitute a
 * library, framework or application as well as the libraries or frameworks used by
 * the former.
 *
 * This class has several purposes:
 *
 *  1. It is a container for `ClassFile`s.
 *  1. It directly gives access to the project's class hierarchy.
 *  1. It serves as a container for project-wide information (e.g., a call graph,
 *     information about the mutability of classes, constant values,...) that can
 *     be queried using [[org.opalj.br.analyses.ProjectInformationKey]]s.
 *     The list of project wide information that can be made available is equivalent
 *     to the list of (concrete/singleton) objects implementing the trait
 *     [[org.opalj.br.analyses.ProjectInformationKey]].
 *     One of the most important project information keys is the
 *     `PropertyStoreKey` which gives access to the property store.
 *
 * ==Thread Safety==
 * This class is thread-safe.
 *
 * ==Prototyping Analyses/Querying Projects==
 * Projects can easily be created and queried using the Scala `REPL`. For example,
 * to create a project, you can use:
 * {{{
 * val project = org.opalj.br.analyses.Project(org.opalj.bytecode.JRELibraryFolder)
 * }}}
 * Now, to determine the number of methods that have at least one parameter of type
 * `int`, you can use:
 * {{{
 * project.methods.filter(_.parameterTypes.exists(_.isIntegerType)).size
 * }}}
 *
 * @tparam Source The type of the source of the class file. E.g., a `URL`, a `File`,
 *         a `String` or a Pair `(JarFile,JarEntry)`. This information is needed for, e.g.,
 *         presenting users meaningful messages w.r.t. the location of issues.
 *         We abstract over the type of the resource to facilitate the embedding in existing
 *         tools such as IDEs. E.g., in Eclipse `IResource`'s are used to identify the
 *         location of a resource (e.g., a source or class file.)
 *
 * @param  logContext The logging context associated with this project. Using the logging
 *         context after the project is no longer referenced (garbage collected) is not
 *         possible.
 *
 * @param classFilesCount The number of classes (including inner and annoymous classes as
 *         well as interfaces, annotations, etc.) defined in libraries and in
 *         the analyzed project.
 *
 * @param methodsCount The number of methods defined in libraries and in the analyzed project.
 *
 * @param fieldsCount The number of fields defined in libraries and in the analyzed project.
 *
 * @param allMethods All methods defined by this project as well as the visible methods
 *                   defined by the libraries.
 * @param allFields All fields defined by this project as well as the reified fields defined
 *                  in libraries.
 * @param allSourceElements `Iterable` over all source elements of the project. The set of all
 *                         source elements consists of (in this order): all methods + all fields
 *                         + all class files.
 *
 * @param libraryClassFilesAreInterfacesOnly If `true` then only the public interfaces
 *         of the methods of the library's classes are available; if `false` all methods and
 *         method bodies are reified.
 *
 * @author Michael Eichberg
 * @author Marco Torsello
 */
class Project[Source] private (
        private[this] val projectModules:             Map[String, ModuleDefinition[Source]], // just contains "module-info" class files
        private[this] val projectClassFiles:          Array[ClassFile], // contains no "module-info" class files
        private[this] val libraryModules:             Map[String, ModuleDefinition[Source]], // just contains "module-info" class files
        private[this] val libraryClassFiles:          Array[ClassFile],
        final val libraryClassFilesAreInterfacesOnly: Boolean,
        private[this] val methodsWithBody:            Array[Method], // methods with bodies sorted by size
        private[this] val methodsWithBodyAndContext:  Array[MethodInfo[Source]], // the concrete methods, sorted by size in descending order
        private[this] val projectTypes:               Set[ObjectType], // the types defined by the class files belonging to the project's code
        private[this] val objectTypeToClassFile:      Map[ObjectType, ClassFile],
        private[this] val sources:                    Map[ObjectType, Source],
        final val projectClassFilesCount:             Int,
        final val projectMethodsCount:                Int,
        final val projectFieldsCount:                 Int,
        final val libraryClassFilesCount:             Int,
        final val libraryMethodsCount:                Int,
        final val libraryFieldsCount:                 Int,
        final val codeSize:                           Long,
        final val MethodHandleSubtypes:               Set[ObjectType],
        final val VarHandleSubtypes:                  Set[ObjectType],
        final val classFilesCount:                    Int,
        final val methodsCount:                       Int,
        final val fieldsCount:                        Int,
        final val allProjectClassFiles:               ArraySeq[ClassFile],
        final val allLibraryClassFiles:               ArraySeq[ClassFile],
        final val allClassFiles:                      Iterable[ClassFile],
        final val allMethods:                         Iterable[Method],
        final val allFields:                          Iterable[Field],
        final val allSourceElements:                  Iterable[SourceElement],
        final val virtualMethodsCount:                Int,
        final val classHierarchy:                     ClassHierarchy,
        final val instanceMethods:                    Map[ObjectType, ArraySeq[MethodDeclarationContext]],
        final val overridingMethods:                  Map[Method, immutable.Set[Method]],
        final val nests:                              Map[ObjectType, ObjectType],
        // Note that the referenced array will never shrink!
        @volatile private[this] var projectInformation: AtomicReferenceArray[AnyRef] = new AtomicReferenceArray[AnyRef](32)
)(
        implicit
        final val logContext: LogContext,
        final val config:     Config
) extends ProjectLike {

    /**
     * Returns a shallow clone of this project with an updated log context and (optionally)
     * filtered ProjectInformation objects.
     *
     * @param filterProjectInformation Enables filtering of the ProjectInformation objects
     *          that should be kept when a new Project is created.
     */
    def recreate(
        filterProjectInformation: Int => Boolean = _ => false
    ): Project[Source] = this.synchronized {
        // the synchronization is necessary to get exclusive access to "project information".
        val max = projectInformation.length()
        val newProjectInformation = new AtomicReferenceArray[AnyRef](max)
        var i = 0
        while (i < max) {
            if (filterProjectInformation(i)) {
                val pi = projectInformation.get(i)
                if (pi != null) {
                    newProjectInformation.set(i, pi)
                }
            }
            i += 1
        }
        val newLogContext = logContext.successor
        val newClassHierarchy = classHierarchy.updatedLogContext(newLogContext)
        new Project(
            projectModules,
            projectClassFiles,
            libraryModules,
            libraryClassFiles,
            libraryClassFilesAreInterfacesOnly,
            methodsWithBody,
            methodsWithBodyAndContext,
            projectTypes,
            objectTypeToClassFile,
            sources,
            projectClassFilesCount,
            projectMethodsCount,
            projectFieldsCount,
            libraryClassFilesCount,
            libraryMethodsCount,
            libraryFieldsCount,
            codeSize,
            MethodHandleSubtypes,
            VarHandleSubtypes,
            classFilesCount,
            methodsCount,
            fieldsCount,
            allProjectClassFiles,
            allLibraryClassFiles,
            allClassFiles,
            allMethods,
            allFields,
            allSourceElements,
            virtualMethodsCount,
            newClassHierarchy,
            instanceMethods,
            overridingMethods,
            nests,
            newProjectInformation
        )(
            newLogContext,
            config
        )
    }

    /* ------------------------------------------------------------------------------------------ *\
    |                                                                                              |
    |                                                                                              |
    |                                     PROJECT STATE                                            |
    |                                                                                              |
    |                                                                                              |
    \* ------------------------------------------------------------------------------------------ */

    final val ObjectClassFile: Option[ClassFile] = classFile(ObjectType.Object)

    final val MethodHandleClassFile: Option[ClassFile] = classFile(ObjectType.MethodHandle)

    final val VarHandleClassFile: Option[ClassFile] = classFile(ObjectType.VarHandle)

    final val allMethodsWithBody: ArraySeq[Method] = ArraySeq.unsafeWrapArray(this.methodsWithBody)

    final val allMethodsWithBodyWithContext: ArraySeq[MethodInfo[Source]] = {
        ArraySeq.unsafeWrapArray(this.methodsWithBodyAndContext)
    }

    /**
     * The set of all classes defined in a specific package.
     */
    // TODO Consider extracting to a ProjectInformationKey
    // TODO Java 9+
    final val classesPerPackage: immutable.Map[String, immutable.Set[ClassFile]] = {
        val classesPerPackage = mutable.Map.empty[String, ArrayBuffer[ClassFile]]
        allClassFiles foreach { cf =>
            val packageName = cf.thisType.packageName
            val buffer =
                classesPerPackage.getOrElse(packageName, {
                    val buffer = ArrayBuffer.empty[ClassFile]
                    classesPerPackage += (packageName -> buffer)
                    buffer
                })
            buffer += cf
        }
        immutable.Map.from(classesPerPackage.iterator.map { case (key, cfs) => (key, cfs.toSet) })
    }

    /**
     * Computes the set of all definitive functional interfaces in a top-down fashion.
     *
     * @see Java 8 language specification for details!
     *
     * @return The functional interfaces.
     */
    // TODO Consider extracting to a ProjectInformationKey
    final lazy val functionalInterfaces: UIDSet[ObjectType] = time {

        // Core idea: a subtype is only processed after processing all supertypes;
        // in case of partial type hierarchies it may happen that all known
        // supertypes are processed, but no all...

        // the set of interfaces that are not functional interfaces themselve, but
        // which can be extended.
        var irrelevantInterfaces = UIDSet.empty[ObjectType]
        val functionalInterfaces = AnyRefMap.empty[ObjectType, MethodSignature]
        var otherInterfaces = UIDSet.empty[ObjectType]

        // our worklist/-set; it only contains those interface types for which
        // we have complete supertype information
        val typesToProcess = classHierarchy.rootInterfaceTypes(mutable.Stack.empty[ObjectType])

        // the given interface type is either not a functional interface or an interface
        // for which we have not enough information
        def noSAMInterface(interfaceType: ObjectType): Unit = {
            // println("non-functional interface: "+interfaceType.toJava)
            // assert(!irrelevantInterfaces.contains(interfaceType))
            // assert(!functionalInterfaces.contains(interfaceType))

            otherInterfaces += interfaceType
            classHierarchy.foreachSubinterfaceType(interfaceType) { i =>
                if (otherInterfaces.contains(i))
                    false
                else {
                    otherInterfaces += i
                    true
                }
            }
        }

        def processSubinterfaces(interfaceType: ObjectType): Unit = {
            classHierarchy.directSubinterfacesOf(interfaceType) foreach { subIType =>
                // println("processing subtype: "+subIType.toJava)
                // let's check if the type is potentially relevant
                if (!otherInterfaces.contains(subIType)) {

                    // only add those types for which we have already derived information for all
                    // superinterface types and which are not already classified..
                    if (classHierarchy.superinterfaceTypes(subIType) match {
                        case Some(superinterfaceTypes) =>
                            superinterfaceTypes.forall { superSubIType =>
                                superSubIType == interfaceType || {
                                    irrelevantInterfaces.contains(superSubIType) ||
                                        functionalInterfaces.contains(superSubIType)
                                }
                            }
                        case None => throw new UnknownError()
                    }) {
                        // we have all information about all supertypes...
                        typesToProcess.push(subIType)
                    }
                }
            }
        }

        def classifyPotentiallyFunctionalInterface(classFile: ClassFile): Unit = {
            if (!classFile.isInterfaceDeclaration) {
                // This may happen for "broken" projects (which we find, e.g., in case of
                // the JDK/Qualitas Corpus).
                noSAMInterface(classFile.thisType)
                return ;
            }
            val interfaceType = classFile.thisType

            val selectAbstractNonObjectMethods = (m: Method) => {
                m.isAbstract && (
                    ObjectClassFile.isEmpty /* in case of doubt we keep it ... */ || {
                        // Does not (re)define a method declared by java.lang.Object;
                        // see java.util.Comparator for an example!
                        // From the spec.: ... The definition of functional interface
                        // excludes methods in an interface that are also public methods
                        // in Object.
                        val objectMethod = ObjectClassFile.get.findMethod(m.name, m.descriptor)
                        objectMethod.isEmpty || !objectMethod.get.isPublic
                    }
                )
            }

            val abstractMethods = classFile.methods.filter(selectAbstractNonObjectMethods)
            val abstractMethodsCount = abstractMethods.size
            val isPotentiallyIrrelevant: Boolean = abstractMethodsCount == 0
            val isPotentiallyFunctionalInterface: Boolean = abstractMethodsCount == 1

            if (!isPotentiallyIrrelevant && !isPotentiallyFunctionalInterface) {
                noSAMInterface(interfaceType)
            } else {
                var sharedFunctionalMethod: MethodSignature = null
                if (classFile.interfaceTypes.forall { i =>
                    //... forall is "only" used to short-cut the evaluation; in case of
                    // false all relevant state is already updated
                    if (!irrelevantInterfaces.contains(i)) {
                        functionalInterfaces.get(i) match {
                            case Some(potentialFunctionalMethod) =>
                                if (sharedFunctionalMethod == null) {
                                    sharedFunctionalMethod = potentialFunctionalMethod
                                    true
                                } else if (sharedFunctionalMethod == potentialFunctionalMethod) {
                                    true
                                } else {
                                    // the super interface types define different abstract methods
                                    noSAMInterface(interfaceType)
                                    false
                                }
                            case None =>
                                // we have a partial type hierarchy...
                                noSAMInterface(interfaceType)
                                false
                        }
                    } else {
                        // the supertype is irrelevant...
                        true
                    }
                }) {
                    // all super interfaces are either irrelevant or share the same
                    // functionalMethod
                    if (sharedFunctionalMethod == null) {
                        if (isPotentiallyIrrelevant)
                            irrelevantInterfaces += interfaceType
                        else
                            functionalInterfaces(interfaceType) = abstractMethods.head.signature
                        processSubinterfaces(interfaceType)
                    } else if (isPotentiallyIrrelevant ||
                        sharedFunctionalMethod == abstractMethods.head.signature) {
                        functionalInterfaces(interfaceType) = sharedFunctionalMethod
                        processSubinterfaces(interfaceType)
                    } else {
                        // different methods are defined...
                        noSAMInterface(interfaceType)
                    }
                }
            }
        }

        while (typesToProcess.nonEmpty) {
            val interfaceType = typesToProcess.pop()

            if (!otherInterfaces.contains(interfaceType) &&
                !functionalInterfaces.contains(interfaceType) &&
                !irrelevantInterfaces.contains(interfaceType)) {

                classFile(interfaceType) match {
                    case Some(classFile) => classifyPotentiallyFunctionalInterface(classFile)
                    case None            => noSAMInterface(interfaceType)
                }
            }
        }

        UIDSet.empty[ObjectType] ++ functionalInterfaces.keys
    } { t =>
        info("project setup", s"computing functional interfaces took ${t.toSeconds}")
    }

    // --------------------------------------------------------------------------------------------
    //
    //    CODE TO MAKE IT POSSIBLE TO ATTACH SOME INFORMATION TO A PROJECT (ON DEMAND)
    //
    // --------------------------------------------------------------------------------------------

    /**
     * Here, the usage of the project information key does not lead to its initialization!
     */
    private[this] val projectInformationKeyInitializationData = {
        new ConcurrentHashMap[ProjectInformationKey[AnyRef, AnyRef], AnyRef]()
    }

    /**
     * Returns the project specific initialization information for the given project information
     * key.
     */
    def getProjectInformationKeyInitializationData[T <: AnyRef, I <: AnyRef](
        key: ProjectInformationKey[T, I]
    ): Option[I] = {
        Option(projectInformationKeyInitializationData.get(key).asInstanceOf[I])
    }

    /**
     * Gets the project information key specific initialization object. If an object is already
     * registered, that object will be used otherwise `info` will be evaluated and that value
     * will be added and also returned.
     *
     * @note    Initialization data is discarded once the key is used.
     */
    def getOrCreateProjectInformationKeyInitializationData[T <: AnyRef, I <: AnyRef](
        key:  ProjectInformationKey[T, I],
        info: => I
    ): I = {
        projectInformationKeyInitializationData.computeIfAbsent(
            key.asInstanceOf[ProjectInformationKey[AnyRef, AnyRef]],
            new java.util.function.Function[ProjectInformationKey[AnyRef, AnyRef], I] {
                def apply(key: ProjectInformationKey[AnyRef, AnyRef]): I = info
            }
        ).asInstanceOf[I]
    }

    /**
     * Updates project information key specific initialization object. If an object is already
     * registered, that object will be given to `info`.
     *
     * @note    Initialization data is discarded once the key is used.
     */
    def updateProjectInformationKeyInitializationData[T <: AnyRef, I <: AnyRef](
        key: ProjectInformationKey[T, I]
    )(
        info: Option[I] => I
    ): I = {
        projectInformationKeyInitializationData.compute(
            key.asInstanceOf[ProjectInformationKey[AnyRef, AnyRef]],
            (_, current: AnyRef) => {
                info(Option(current.asInstanceOf[I]))
            }: I
        ).asInstanceOf[I]
    }

    /**
     * Returns the additional project information that is ''currently'' available.
     *
     * If some analyses are still running it may be possible that additional
     * information will be made available as part of the execution of those
     * analyses.
     *
     * @note This method redetermines the available project information on each call.
     */
    def availableProjectInformation: List[AnyRef] = {
        var pis = List.empty[AnyRef]
        val projectInformation = this.projectInformation
        for (i <- (0 until projectInformation.length())) {
            val pi = projectInformation.get(i)
            if (pi != null) {
                pis = pi :: pis
            }
        }
        pis
    }

    /**
     * Returns the information attached to this project that is identified by the
     * given `ProjectInformationKey`.
     *
     * If the information was not yet required, the information is computed and
     * returned. Subsequent calls will directly return the information.
     *
     * @note    (Development Time)
     *          Every analysis using [[ProjectInformationKey]]s must list '''All
     *          requirements; failing to specify a requirement can end up in a deadlock.'''
     *
     * @see     [[ProjectInformationKey]] for further information.
     */
    def get[T <: AnyRef](pik: ProjectInformationKey[T, _]): T = {
        val pikUId = pik.uniqueId

        /* Synchronization is done by the caller! */
        def derive(projectInformation: AtomicReferenceArray[AnyRef]): T = {
            var className = pik.getClass.getSimpleName
            if (className.endsWith("Key"))
                className = className.substring(0, className.length - 3)
            else if (className.endsWith("Key$"))
                className = className.substring(0, className.length - 4)

            for (requiredProjectInformationKey <- pik.requirements(this)) {
                get(requiredProjectInformationKey)
            }
            val pi = time {
                val pi = pik.compute(this)
                // we don't need the initialization data anymore
                projectInformationKeyInitializationData.remove(pik)
                pi
            } { t => info("project", s"initialization of $className took ${t.toSeconds}") }
            projectInformation.set(pikUId, pi)
            pi
        }

        val projectInformation = this.projectInformation
        if (pikUId < projectInformation.length()) {
            val pi = projectInformation.get(pikUId)
            if (pi ne null) {
                pi.asInstanceOf[T]
            } else {
                this.synchronized {
                    // It may be the case that the underlying array was replaced!
                    val projectInformation = this.projectInformation
                    // double-checked locking (works with Java >=6)
                    val pi = projectInformation.get(pikUId)
                    if (pi ne null) {
                        pi.asInstanceOf[T]
                    } else {
                        derive(projectInformation)
                    }
                }
            }
        } else {
            // We have to synchronize w.r.t. "this" object on write accesses
            // to make sure that we do not loose a concurrent update or
            // derive an information more than once.
            this.synchronized {
                val projectInformation = this.projectInformation
                if (pikUId >= projectInformation.length()) {
                    val newLength = Math.max(projectInformation.length * 2, pikUId * 2)
                    val newProjectInformation = new AtomicReferenceArray[AnyRef](newLength)
                    org.opalj.control.iterateUntil(0, projectInformation.length()) { i =>
                        newProjectInformation.set(i, projectInformation.get(i))
                    }
                    this.projectInformation = newProjectInformation
                    return derive(newProjectInformation);
                }
            }
            // else (pikUId < projectInformation.length()) => the underlying array is "large enough"
            get(pik)
        }
    }

    /**
     * Tests if the information identified by the given [[ProjectInformationKey]]
     * is available. If the information is not (yet) available, the information
     * will not be computed; `None` will be returned.
     *
     * @see [[ProjectInformationKey]] for further information.
     */
    def has[T <: AnyRef](pik: ProjectInformationKey[T, _]): Option[T] = {
        val pikUId = pik.uniqueId

        if (pikUId < this.projectInformation.length())
            Option(this.projectInformation.get(pikUId).asInstanceOf[T])
        else
            None
    }

    OPALLogger.debug("progress", s"project created (${logContext.logContextId})")

    /* ------------------------------------------------------------------------------------------ *\
    |                                                                                              |
    |                                                                                              |
    |                                    INSTANCE METHODS                                          |
    |                                                                                              |
    |                                                                                              |
    \* ------------------------------------------------------------------------------------------ */

    /**
     * Creates a new `Project` which also includes the given class files.
     */
    def extend(projectClassFilesWithSources: Iterable[(ClassFile, Source)]): Project[Source] = {
        Project.extend[Source](this, projectClassFilesWithSources)
    }

    /**
     * Creates a new `Project` which also includes this as well as the other project's
     * class files.
     */
    def extend(other: Project[Source]): Project[Source] = {
        if (this.libraryClassFilesAreInterfacesOnly != other.libraryClassFilesAreInterfacesOnly) {
            throw new IllegalArgumentException("the projects' libraries are loaded differently");
        }

        val otherClassFiles = other.projectClassFilesWithSources
        val otherLibraryClassFiles = other.libraryClassFilesWithSources
        Project.extend[Source](this, otherClassFiles, otherLibraryClassFiles)
    }

    /**
     * The number of all source elements (fields, methods and class files).
     */
    def sourceElementsCount: Int = fieldsCount + methodsCount + classFilesCount

    private[this] def doParForeachClassFile[T](
        classFiles: Array[ClassFile], isInterrupted: () => Boolean
    )(
        f: ClassFile => T
    ): Unit = {
        val classFilesCount = classFiles.length
        if (classFilesCount == 0)
            return ;

        parForeachArrayElement(classFiles, NumberOfThreadsForCPUBoundTasks, isInterrupted)(f)
    }

    def parForeachProjectClassFile[T](
        isInterrupted: () => Boolean = defaultIsInterrupted
    )(
        f: ClassFile => T
    ): Unit = {
        doParForeachClassFile(this.projectClassFiles, isInterrupted)(f)
    }

    def parForeachLibraryClassFile[T](
        isInterrupted: () => Boolean = defaultIsInterrupted
    )(
        f: ClassFile => T
    ): Unit = {
        doParForeachClassFile(this.libraryClassFiles, isInterrupted)(f)
    }

    def parForeachClassFile[T](
        isInterrupted: () => Boolean = defaultIsInterrupted
    )(
        f: ClassFile => T
    ): Unit = {
        parForeachProjectClassFile(isInterrupted)(f)
        parForeachLibraryClassFile(isInterrupted)(f)
    }

    /**
     * The set of all method names of the given types.
     */
    def methodNames(objectTypes: Iterable[ObjectType]): Set[String] = {
        objectTypes.flatMap(ot => classFile(ot)).flatMap(cf => cf.methods.map(m => m.name)).toSet
    }

    /**
     * Returns the list of all packages that contain at least one class.
     *
     * For example, in case of the JDK the package `java` does not directly contain
     * any class – only its subclasses. This package is, hence, not returned by this
     * function, but the package `java.lang` is.
     *
     * @note This method's result is not cached.
     */
    def packages: Set[String] = projectPackages ++ libraryPackages

    /**
     * Returns the set of all project packages that contain at least one class.
     *
     * For example, in case of the JDK the package `java` does not directly contain
     * any class – only its subclasses. This package is, hence, not returned by this
     * function, but the package `java.lang` is.
     *
     * @note This method's result is not cached.
     */
    def projectPackages: Set[String] = {
        projectClassFiles.foldLeft(mutable.Set.empty[String])(_ addOne _.thisType.packageName)
    }

    /**
     * Returns the set of all library packages that contain at least one class.
     *
     * For example, in case of the JDK the package `java` does not directly contain
     * any class – only its subclasses. This package is, hence, not returned by this
     * function, but the package `java.lang` is.
     *
     * @note This method's result is not cached.
     */
    def libraryPackages: Set[String] = {
        libraryClassFiles.foldLeft(mutable.Set.empty[String])(_ addOne _.thisType.packageName)
    }

    /**
     * Iterates over all methods with a body in parallel starting with the largest methods first.
     *
     * This method maximizes utilization by allowing each thread to pick the next
     * unanalyzed method as soon as the thread has finished analyzing the previous method.
     * I.e., each thread is not assigned a fixed batch of methods. Additionally, the
     * methods are analyzed ordered by their length (longest first).
     */
    def parForeachMethodWithBody[T](
        isInterrupted:        () => Boolean = defaultIsInterrupted,
        parallelizationLevel: Int           = NumberOfThreadsForCPUBoundTasks
    )(
        f: MethodInfo[Source] => T
    ): Unit = {
        val methods = this.methodsWithBodyAndContext
        if (methods.length == 0)
            return ;

        parForeachArrayElement(methods, parallelizationLevel, isInterrupted)(f)
    }

    /**
     * Iterates over all methods in parallel; actually, the methods belonging to a specific class
     * are analyzed sequentially..
     */
    def parForeachMethod[T](
        isInterrupted: () => Boolean = defaultIsInterrupted
    )(
        f: Method => T
    ): Unit = {
        parForeachClassFile(isInterrupted) { cf => cf.methods.foreach(f) }
    }

    /**
     * Determines for all packages of this project that contain at least one class
     * the "root" packages and stores the mapping between the package and its root package.
     *
     * For example, let's assume that we have project which has the following packages
     * that contain at least one class:
     *  - org.opalj
     *  - org.opalj.ai
     *  - org.opalj.ai.domain
     *  - org.apache.commons.io
     *  - java.lang
     * Then the map will be:
     *  - org.opalj => org.opalj
     *  - org.opalj.ai => org.opalj
     *  - org.opalj.ai.domain => org.opalj
     *  - org.apache.commons.io => org.apache.commons.io
     *  - java.lang => java.lang
     *
     * In other words the set of rootPackages can then be determined using:
     * {{{
     * .rootPackages().values.toSet
     * }}}
     *
     * @note This method's result is not cached.
     *
     * @return a Map which contains for each package name the root package name.
     */
    def rootPackages: Map[String, String] = {
        val allPackages = packages.toSeq.sorted
        if (allPackages.isEmpty)
            Map.empty
        else if (allPackages.tail.isEmpty)
            Map((allPackages.head, allPackages.head))
        else {
            allPackages.tail.foldLeft(immutable.SortedMap((allPackages.head, allPackages.head))) {
                (rootPackages, nextPackage) =>
                    // java is not a root package of "javax"...
                    val (_, lastPackage) = rootPackages.last
                    if (nextPackage.startsWith(lastPackage) &&
                        nextPackage.charAt(lastPackage.size) == '/')
                        rootPackages + ((nextPackage, lastPackage))
                    else
                        rootPackages + ((nextPackage, nextPackage))
            }
        }
    }

    /**
     * Number of packages.
     *
     * @note The result is (re)calculated for each call.
     */
    def packagesCount = packages.size

    /**
     * Distributes all classes which define methods with bodies across a given number of
     * groups. Afterwards these groups can, e.g., be processed in parallel.
     */
    def groupedClassFilesWithMethodsWithBody(groupsCount: Int): Array[Buffer[ClassFile]] = {
        var nextGroupId = 0
        val groups = Array.fill[Buffer[ClassFile]](groupsCount) {
            new ArrayBuffer[ClassFile](methodsCount / groupsCount)
        }
        for {
            classFile <- projectClassFiles
            if classFile.methods.exists(_.body.isDefined)
        } {
            // we distribute the classfiles among the different bins
            // to avoid that one bin accidentally just contains
            // interfaces
            groups(nextGroupId) += classFile
            nextGroupId = (nextGroupId + 1) % groupsCount
        }
        groups
    }

    def projectClassFilesWithSources: Iterable[(ClassFile, Source)] = {
        projectClassFiles.view.map { classFile => (classFile, sources(classFile.thisType)) }
    }

    def libraryClassFilesWithSources: Iterable[(ClassFile, Source)] = {
        libraryClassFiles.view.map { classFile => (classFile, sources(classFile.thisType)) }
    }

    def classFilesWithSources: Iterable[(ClassFile, Source)] = {
        projectClassFilesWithSources ++ libraryClassFilesWithSources
    }

    /**
     * Returns `true` if the given class file belongs to the library part of the project.
     * This is only the case if the class file was explicitly identified as being
     * part of the library. By default all class files are considered to belong to the
     * code base that will be analyzed.
     */
    def isLibraryType(classFile: ClassFile): Boolean = isLibraryType(classFile.thisType)

    /**
     * Returns `true` if the given type belongs to the library part of the project.
     * This is generally the case if no class file was loaded for the given type.
     */
    def isLibraryType(objectType: ObjectType): Boolean = !projectTypes.contains(objectType)

    /**
     * Returns `true` iff the given type belongs to the project and not to a library.
     */
    def isProjectType(objectType: ObjectType): Boolean = projectTypes.contains(objectType)

    /**
     * Returns the source (for example, a `File` object or `URL` object) from which
     * the class file was loaded that defines the given object type, if any.
     *
     * @param objectType Some object type.
     */
    def source(objectType: ObjectType): Option[Source] = sources.get(objectType)
    def source(classFile: ClassFile): Option[Source] = source(classFile.thisType)

    /**
     * Returns the class file that defines the given `objectType`; if any.
     *
     * @param objectType Some object type.
     */
    override def classFile(objectType: ObjectType): Option[ClassFile] = {
        objectTypeToClassFile.get(objectType)
    }

    /**
     * Returns all available `ClassFile` objects for the given `objectTypes` that
     * pass the given `filter`. `ObjectType`s for which no `ClassFile` is available
     * are ignored.
     */
    def lookupClassFiles(
        objectTypes: Iterable[ObjectType]
    )(
        classFileFilter: ClassFile => Boolean
    ): Iterable[ClassFile] = {
        objectTypes.view.flatMap(classFile) filter (classFileFilter)
    }

    def hasInstanceMethod(
        receiverType:     ObjectType,
        name:             String,
        descriptor:       MethodDescriptor,
        isPackagePrivate: Boolean
    ): Boolean = {
        val data = instanceMethods(receiverType)

        @tailrec @inline def binarySearch(low: Int, high: Int): Int = {
            if (high < low)
                return -1;

            val mid = (low + high) / 2 // <= will never overflow...(by constraint...)
            val e = data(mid)
            val eComparison = e.method.compare(name, descriptor)
            if (eComparison == 0) {
                mid
            } else if (eComparison < 0) {
                binarySearch(mid + 1, high)
            } else {
                binarySearch(low, mid - 1)
            }
        }

        val candidateIndex = binarySearch(0, data.length - 1)
        // In case we found a method, but it is not `method.isPackagePrivate` == `isPackagePrivate`,
        // it is possible that there is another method with the same name and descriptor next
        // to that one (i.e. left or right).
        // Therefore, we also check if there exists such a method, with indices lower/higher to
        // the found one.
        candidateIndex != -1 && {
            var index = candidateIndex
            var method: Method = null
            // check the methods with a smaller (or equal) index
            while (index >= 0
                && { method = data(index).method; method.compare(name, descriptor) == 0 }) {
                if (method.isPackagePrivate == isPackagePrivate)
                    return true;
                index -= 1
            }

            index = candidateIndex + 1 // reset the index

            // check the methods with a higher index
            while (index < data.length
                && { method = data(index).method; method.compare(name, descriptor) == 0 }) {
                if (method.isPackagePrivate == isPackagePrivate)
                    return true;

                index += 1
            }

            false
        }
    }

    /**
     * Converts this project abstraction into a standard Java `HashMap`.
     *
     * @note This method is intended to be used by Java projects that want to interact with OPAL.
     */
    def toJavaMap(): java.util.HashMap[ObjectType, ClassFile] = {
        val map = new java.util.HashMap[ObjectType, ClassFile]
        for (classFile <- allClassFiles) map.put(classFile.thisType, classFile)
        map
    }

    /**
     * Some basic statistics about this project.
     *
     * ((Re)Calculated on-demand.)
     */
    def statistics: Map[String, Int] = {
        Map(
            ("ProjectClassFiles" -> projectClassFilesCount),
            ("LibraryClassFiles" -> libraryClassFilesCount),
            ("ProjectMethods" -> projectMethodsCount),
            ("ProjectFields" -> projectFieldsCount),
            ("LibraryMethods" -> libraryMethodsCount),
            ("LibraryFields" -> libraryFieldsCount),
            ("ProjectPackages" -> projectPackages.size),
            ("LibraryPackages" -> libraryPackages.size),
            ("ProjectInstructions" ->
                projectClassFiles.foldLeft(0)(_ + _.methods.view.filter(_.body.isDefined).
                    foldLeft(0)(_ + _.body.get.instructions.count(_ != null))))
        )
    }

    /**
     * Returns the (number of) (non-synthetic) methods per method length
     * (size in length of the method's code array).
     */
    def projectMethodsLengthDistribution: Map[Int, Set[Method]] = {
        val nonSyntheticMethodsWithBody = methodsWithBody.iterator.filterNot(_.isSynthetic)
        val data = immutable.SortedMap.empty[Int, immutable.Set[Method]]
        nonSyntheticMethodsWithBody.foldLeft(data) { (data, method) =>
            val methodLength = method.body.get.instructions.length
            val methods = data.getOrElse(methodLength, immutable.Set.empty[Method])
            data + ((methodLength, methods + method))
        }
    }

    /**
     * Returns the number of (non-synthetic) fields and methods per class file.
     * The number of class members of nested classes is also taken into consideration.
     * I.e., the map's key identifies the category and the value is a pair where the first value
     * is the count and the value is the names of the source elements.
     *
     * The count can be higher than the set of names of class members due to method overloading.
     */
    def projectClassMembersPerClassDistribution: Map[Int, (Int, Set[String])] = {
        val data = AnyRefMap.empty[String, Int]

        projectClassFiles foreach { classFile =>
            // we want to collect the size in relation to the source code;
            //i.e., across all nested classes
            val count =
                classFile.methods.iterator.filterNot(_.isSynthetic).size +
                    classFile.fields.iterator.filterNot(_.isSynthetic).size

            var key = classFile.thisType.toJava
            if (classFile.isInnerClass) {
                val index = key.indexOf('$')
                if (index >= 0) {
                    key = key.substring(0, index)
                }
            }
            data.update(key, data.getOrElse(key, 0) + count + 1 /*+1 for the inner class*/ )
        }

        var result = immutable.SortedMap.empty[Int, (Int, immutable.Set[String])]
        for ((typeName, membersCount) <- data) {
            val (count, typeNames) = result.getOrElse(membersCount, (0, immutable.Set.empty[String]))
            result += ((membersCount, (count + 1, typeNames + typeName)))
        }
        result
    }

    override def toString: String = {
        val classDescriptions =
            sources map { entry =>
                val (ot, source) = entry
                ot.toJava+" « "+source.toString
            }

        classDescriptions.mkString(
            "Project("+
                "\n\tlibraryClassFilesAreInterfacesOnly="+libraryClassFilesAreInterfacesOnly+
                "\n\t",
            "\n\t",
            "\n)"
        )
    }

    // ----------------------------------------------------------------------------------
    //
    // FINALIZATION
    //
    // ----------------------------------------------------------------------------------

    /**
     * Unregisters this project from the OPALLogger and then calls `super.finalize`.
     */
    override protected def finalize(): Unit = {
        OPALLogger.debug("project", "finalized ("+logContext+")")
        if (logContext != GlobalLogContext) { OPALLogger.unregister(logContext) }

        // DEPRECATED: super.finalize()
        // The "correct" solution requires Java 9 (Cleaner) - we want to remain compatible
        // Java 8 for the time being; hence, we will keep it as it is for the time being.
    }
}

/**
 * Definition of factory methods to create [[Project]]s.
 *
 * @author Michael Eichberg
 */
object Project {

    lazy val JavaLibraryClassFileReader: Java17LibraryFramework.type = Java17LibraryFramework

    @volatile private[this] var theCache: SoftReference[BytecodeInstructionsCache] = {
        new SoftReference(new BytecodeInstructionsCache)
    }
    private[this] def cache: BytecodeInstructionsCache = {
        var cache = theCache.get
        if (cache == null) {
            this.synchronized {
                cache = theCache.get
                if (cache == null) {
                    cache = new BytecodeInstructionsCache
                    theCache = new SoftReference(cache)
                }
            }
        }
        cache
    }

    def JavaClassFileReader(
        implicit
        theLogContext: LogContext = GlobalLogContext,
        theConfig:     Config     = BaseConfig
    ): Java17FrameworkWithDynamicRewritingAndCaching = {
        // The following makes use of early initializers
        class ConfiguredFramework extends Java17FrameworkWithDynamicRewritingAndCaching(cache) {
            override def defaultLogContext: LogContext = theLogContext
            override def defaultConfig: Config = theConfig
        }
        new ConfiguredFramework
    }

    /**
     * Performs some fundamental validations to make sure that subsequent analyses don't have
     * to deal with completely broken projects/that the user is aware of the issues!
     */
    private[this] def validate(project: SomeProject): Seq[InconsistentProjectException] = {

        implicit val logContext = project.logContext

        val disclaimer = "(this inconsistency may lead to useless/wrong results)"

        import project.classHierarchy
        import classHierarchy.isInterface

        var exs = List.empty[InconsistentProjectException]
        val exsMutex = new Object
        def addException(ex: InconsistentProjectException): Unit = {
            exsMutex.synchronized { exs = ex :: exs }
        }

        try {
            project.parForeachMethodWithBody(() => Thread.interrupted()) { mi =>
                val m: Method = mi.method
                val cf = m.classFile

                def completeSupertypeInformation =
                    classHierarchy.isSupertypeInformationComplete(cf.thisType)

                def missingSupertypeClassFile =
                    classHierarchy.allSupertypes(cf.thisType, false).find { t =>
                        project.classFile(t).isEmpty
                    }.map { ot =>
                        (classHierarchy.isInterface(ot) match {
                            case Yes     => "interface "
                            case No      => "class "
                            case Unknown => "interface/class "
                        }) + ot.toJava
                    }.getOrElse("")

                m.body.get iterate { (pc: Int, instruction: Instruction) =>

                    def validateReceiverTypeKind(
                        invoke: NonVirtualMethodInvocationInstruction
                    ): Boolean = {
                        val typeIsInterface = isInterface(invoke.declaringClass.asObjectType)
                        if (typeIsInterface.isYesOrNo && typeIsInterface.isYes != invoke.isInterfaceCall) {
                            val ex = InconsistentProjectException(
                                s"the type of the declaring class of the target method of the invokes call in "+
                                    m.toJava(s"pc=$pc; $invoke - $disclaimer")+
                                    " is inconsistent; it is expected to be "+
                                    (if (invoke.isInterfaceCall) "an interface" else "a class"),
                                Error
                            )
                            addException(ex)
                            false
                        } else {
                            true
                        }
                    }

                    try {
                        (instruction.opcode: @switch) match {

                            case NEW.opcode =>
                                val NEW(objectType) = instruction
                                if (isInterface(objectType).isYes) {
                                    val ex = InconsistentProjectException(
                                        s"cannot create an instance of interface ${objectType.toJava} in "+
                                            m.toJava(s"pc=$pc $disclaimer"),
                                        Error
                                    )
                                    addException(ex)
                                }

                            case INVOKESTATIC.opcode =>
                                val invokestatic = instruction.asInstanceOf[INVOKESTATIC]
                                if (validateReceiverTypeKind(invokestatic)) {
                                    project.staticCall(cf.thisType, invokestatic) match {
                                        case _: Success[_] => /*OK*/
                                        case Empty         => /*OK - partial project*/
                                        case Failure =>
                                            val ex = InconsistentProjectException(
                                                s"target method of invokestatic call in "+
                                                    m.toJava(s"pc=$pc; $invokestatic - $disclaimer")+
                                                    " cannot be resolved; supertype information is complete="+
                                                    completeSupertypeInformation+
                                                    "; missing supertype class file: "+missingSupertypeClassFile,
                                                Error
                                            )
                                            addException(ex)
                                    }
                                }

                            case INVOKESPECIAL.opcode =>
                                val invokespecial = instruction.asInstanceOf[INVOKESPECIAL]
                                if (validateReceiverTypeKind(invokespecial)) {
                                    project.specialCall(cf.thisType, invokespecial) match {
                                        case _: Success[_] => /*OK*/
                                        case Empty         => /*OK - partial project*/
                                        case Failure =>
                                            val ex = InconsistentProjectException(
                                                s"target method of invokespecial call in "+
                                                    m.toJava(s"pc=$pc; $invokespecial - $disclaimer")+
                                                    " cannot be resolved; supertype information is complete="+
                                                    completeSupertypeInformation+
                                                    "; missing supertype class file: "+missingSupertypeClassFile,
                                                Error
                                            )
                                            addException(ex)
                                    }
                                }
                            case _ => // Nothing special is checked (so far)
                        }
                    } catch {
                        case t: Throwable =>
                            OPALLogger.error(
                                "OPAL",
                                s"project validation of ${m.toJava(s"pc=$pc/$instruction")} failed unexpectedly",
                                t
                            )
                    }
                }
            }
        } catch {
            case ce: ConcurrentExceptions =>
                ce.getSuppressed foreach { e =>
                    error("internal - ignored", "project validation failed", e)
                }
        }

        exs
    }

    /**
     * The type of the function that is called if an inconsistent project is detected.
     */
    type HandleInconsistentProject = (LogContext, InconsistentProjectException) => Unit

    /**
     * This default handler just "logs" inconsistent project exceptions at the
     * [[org.opalj.log.Warn]] level.
     */
    def defaultHandlerForInconsistentProjects(
        logContext: LogContext,
        ex:         InconsistentProjectException
    ): Unit = {
        OPALLogger.log(ex.severity("project configuration", ex.message))(logContext)
    }

    def instanceMethods(
        classHierarchy:        ClassHierarchy,
        objectTypeToClassFile: ObjectType => Option[ClassFile]
    )(
        implicit
        logContext: LogContext
    ): Map[ObjectType, ArraySeq[MethodDeclarationContext]] = time {

        import org.opalj.br.analyses.ProjectLike.findMaximallySpecificSuperinterfaceMethods

        // IDEA
        // Process the type hierarchy starting with the root type(s) to ensure that all method
        // information about all super types is available (already stored in instanceMethods)
        // when we process the subtype. If not all information is already available, which
        // can happen in the following case if the processing of C would be scheduled before B:
        //      interface A; interface B extends A; interface C extends A, B,
        // we postpone the processing of C until the information is available.

        val methods: AnyRefMap[ObjectType, List[MethodDeclarationContext]] = {
            new AnyRefMap(ObjectType.objectTypesCount)
        }

        // Here, "overridden" is to be taken with a grain of salt, because we have a static
        // method with the same name and descriptor as an instance method defined by a super
        // class...
        var staticallyOverriddenInstanceMethods: List[(ObjectType, String, MethodDescriptor)] = Nil

        var missingClassTypes = immutable.Set.empty[ObjectType]

        // Returns `true` if the potentially available information is not yet available.
        @inline def notYetAvailable(superinterfaceType: ObjectType): Boolean = {
            methods.get(superinterfaceType).isEmpty &&
                // If the class file is not known, we will never have any details;
                // hence, the information will "NEVER" be available; or - in other
                // words - all potentially available information is available.
                objectTypeToClassFile(superinterfaceType).nonEmpty
        }

        def computeDefinedMethods(tasks: Tasks[ObjectType], objectType: ObjectType): Unit = {
            // Due to the fact that we may inherit from multiple interfaces,
            // the computation may have been scheduled multiple times; hence, if we are
            // already done, just return.
            if (methods.get(objectType).nonEmpty)
                return ;

            val superclassType = classHierarchy.superclassType(objectType)

            val inheritedClassMethods: List[MethodDeclarationContext] =
                if (superclassType.isDefined) {
                    val theSuperclassType = superclassType.get
                    if (notYetAvailable(theSuperclassType)) {
                        // let's postpone the processing of this object type
                        // because we will get some result in the future
                        tasks.submit(objectType)
                        return ;
                    }
                    val superclassTypeMethods = methods.get(theSuperclassType)
                    if (superclassTypeMethods.nonEmpty) {
                        val inheritedClassMethods = superclassTypeMethods.get
                        if (classHierarchy.isInterface(objectType).isYes) {
                            // an interface does not inherit non-public methods from java.lang.Object
                            inheritedClassMethods.filter(mdc => mdc.method.isPublic)
                        } else {
                            inheritedClassMethods
                        }
                    } else
                        List.empty
                } else {
                    List.empty
                }

            // We have to select the most maximally specific methods, recall that:
            //  -   methods defined by a class have precedence over concrete methods defined
            //      by interfaces (e.g., default methods).
            //  -   an abstract method defined by an interface "nullifies" a concrete
            //      package-visible or protected visible method defined by a superclass.
            //  -   we assume that the project is valid; i.e., there is
            //      always at most one maximally specific method and if not, then
            //      the subclass resolves the conflict by defining the method.
            var definedMethods: List[MethodDeclarationContext] = inheritedClassMethods

            // Note that we must NOT process interfaces again that were already implemented by the
            // supertype because the supertype can make a default method "abstract" again
            val superinterfaceTypes = classHierarchy.allSuperinterfacetypes(objectType) --
                superclassType.map(classHierarchy.allSuperinterfacetypes(_)).getOrElse(UIDSet.empty)

            // We have to filter (remove) those interfaces that are directly and indirectly
            // inherited. In this case the potentially(!) correct method is defined by the interface
            // which also implements the indirectly inherited interface!
            // Concrete case:
            // interface S { default void m(){;} }
            // interface SL extends S { abstract void m(); /* m is made abstract!!! */ }
            // interface SR extends S { }
            // The concrete method m defined by S does NOT belong to the interface of SB(!):
            // interface SB extends SL,SR { }
            //
            // Hence, when we have to find the correct method, we first have to determine
            // that - in case of SB - the only relevant super interfaces are SL and SR, but
            // not S.

            def processMaximallySpecificSuperinterfaceMethod(
                inheritedInterfaceMethod: Method
            ): Unit = {
                // The relevant interface methods are public, hence, the package
                // name is not relevant!
                definedMethods find { definedMethod =>
                    definedMethod.descriptor == inheritedInterfaceMethod.descriptor &&
                        definedMethod.name == inheritedInterfaceMethod.name
                } match {
                    case Some(mdc) =>
                        // If there is already a method and it is from an interface, then it is not
                        // maximally specific and must be replaced. If it is from a class however, we
                        // must keep it.

                        if (mdc.method.classFile.isInterfaceDeclaration) {
                            definedMethods = definedMethods filterNot { definedMethod =>
                                definedMethod.descriptor == inheritedInterfaceMethod.descriptor &&
                                    definedMethod.name == inheritedInterfaceMethod.name
                            }
                            if (!inheritedInterfaceMethod.isAbstract)
                                definedMethods ::= MethodDeclarationContext(inheritedInterfaceMethod)
                        }
                    case None =>
                        if (!inheritedInterfaceMethod.isAbstract)
                            definedMethods ::= MethodDeclarationContext(inheritedInterfaceMethod)
                }
            }

            var interfaceMethods: immutable.Set[MethodSignature] = immutable.Set.empty
            var uniqueInterfaceMethods: immutable.Set[Method] = immutable.Set.empty
            var uniqueInterfaceMethodSignatures: immutable.Set[MethodSignature] = immutable.Set.empty
            for {
                superinterfaceType <- superinterfaceTypes
                superinterfaceClassfile <- objectTypeToClassFile(superinterfaceType)
                superinterfaceTypeMethod <- superinterfaceClassfile.methods
                if superinterfaceTypeMethod.isPublic &&
                    !superinterfaceTypeMethod.isStatic &&
                    !superinterfaceTypeMethod.isInitializer
            } {
                val signature = superinterfaceTypeMethod.signature
                if (interfaceMethods.contains(signature)) {
                    uniqueInterfaceMethodSignatures -= signature
                    uniqueInterfaceMethods = uniqueInterfaceMethods.filterNot { m =>
                        m.signature == signature
                    }
                } else {
                    interfaceMethods += signature
                    uniqueInterfaceMethodSignatures += signature
                    uniqueInterfaceMethods += superinterfaceTypeMethod
                }
            }

            uniqueInterfaceMethods foreach { m => processMaximallySpecificSuperinterfaceMethod(m) }

            // let's keep the contexts related to the maximally specific methods.
            /* OLD
            interfaceMethods.iterator.filterNot { ms =>
                uniqueInterfaceMethodSignatures.contains(ms)
            } foreach { interfaceMethod =>
            */
            interfaceMethods foreach { interfaceMethod =>
                if (!uniqueInterfaceMethodSignatures.contains(interfaceMethod)) {
                    val (_, maximallySpecificSuperiniterfaceMethod) =
                        findMaximallySpecificSuperinterfaceMethods(
                            superinterfaceTypes,
                            interfaceMethod.name, interfaceMethod.descriptor,
                            UIDSet.empty[ObjectType]
                        )(objectTypeToClassFile, classHierarchy, logContext)
                    if (maximallySpecificSuperiniterfaceMethod.size == 1) {
                        // A maximally specific interface method can only be invoked if it is unique!
                        processMaximallySpecificSuperinterfaceMethod(
                            maximallySpecificSuperiniterfaceMethod.head
                        )
                    }
                }
            }

            objectTypeToClassFile(objectType) match {
                case Some(classFile) =>
                    for { declaredMethod <- classFile.methods } {
                        if (declaredMethod.isVirtualMethodDeclaration) {
                            val declaredMethodContext = MethodDeclarationContext(declaredMethod)
                            // We have to filter multiple methods when we inherit (w.r.t. the
                            // visibility) multiple conflicting methods!
                            definedMethods = definedMethods.filterNot { mdc =>
                                declaredMethodContext.directlyOverrides(mdc) ||
                                    mdc.method.isPrivate &&
                                    declaredMethodContext.method.compare(mdc.method) == 0
                            }

                            // Recall that it is possible to make a method "abstract" again...
                            if (declaredMethod.isNotAbstract) {
                                definedMethods ::= declaredMethodContext
                            }
                        } else if (declaredMethod.isStatic) {
                            val declaredMethodName = declaredMethod.name
                            val declaredMethodDescriptor = declaredMethod.descriptor
                            if (definedMethods.exists { mdc =>
                                mdc.name == declaredMethodName &&
                                    mdc.descriptor == declaredMethodDescriptor
                            }) {
                                // In this case we may have an "overriding" of an instance method by
                                // a static method defined by the current interface or class type.
                                // If so – we have to remove the instance method from the set
                                // of defined methods for THIS SPECIFIC CLASS/INTERFACE ONlY; however,
                                // we can only remove it later on, because the instance method is
                                // visible again in subclasses/subinterfaces and we therefore
                                // first have to propagate it.
                                staticallyOverriddenInstanceMethods ::=
                                    ((objectType, declaredMethodName, declaredMethodDescriptor))
                            }
                        } else if (!declaredMethod.isInitializer) {
                            // Private methods can be invoked by invokevirtual instructions (and
                            // invokeinterface for Java 11+). If a call is resolved to a private
                            // method, it is performed non-virtually, thus private methods
                            // effectively shadow inherited methods (only possible in code evolution
                            // scenarios)
                            val declaredMethodContext = MethodDeclarationContext(declaredMethod)
                            definedMethods = definedMethods.filter { mdc =>
                                declaredMethodContext.method.compare(mdc.method) != 0
                            }

                            // Recall that it is possible to make a method "abstract" again...
                            if (declaredMethod.isNotAbstract) {
                                definedMethods ::= declaredMethodContext
                            }
                        }
                    }

                case None =>
                    // ... reached only in case of rather incomplete projects...
                    missingClassTypes += objectType
            }
            methods(objectType) = definedMethods
            classHierarchy.foreachDirectSubtypeOf(objectType)(tasks.submit)
        }

        val tasks = new SequentialTasks[ObjectType](computeDefinedMethods, abortOnExceptions = true)
        classHierarchy.rootTypes.foreach(tasks.submit)
        try {
            tasks.join()
            if (missingClassTypes.nonEmpty) {
                OPALLogger.warn(
                    "project configuration - instance methods",
                    missingClassTypes
                        .map(_.toJava)
                        .toList.sorted
                        .take(10)
                        .mkString(
                            "no class files found for: {",
                            ", ",
                            if (missingClassTypes.size > 10) ", ...}" else "}"
                        )
                )
            }
        } catch {
            case ce: ConcurrentExceptions =>
                ce.getSuppressed foreach { e =>
                    error("project setup", "computing the defined methods failed", e)
                }
        }

        staticallyOverriddenInstanceMethods foreach { sodm =>
            val (declaringType, name, descriptor) = sodm
            methods(declaringType) =
                methods(declaringType) filter { mdc =>
                    mdc.descriptor != descriptor || mdc.name != name
                }
        }

        val result = methods.mapValuesNow { mdcs =>
            val sortedMethods = mdcs.toArray
            sortArray(sortedMethods, MethodDeclarationContextOrdering)
            ArraySeq.unsafeWrapArray(sortedMethods)
        }
        result.repack()
        result
    } { t => info("project setup", s"computing defined methods took ${t.toSeconds}") }

    /**
     * Returns for a given virtual method the set of all non-abstract virtual methods which
     * override it.
     *
     * This method takes the visibility of the methods and the defining context into consideration.
     *
     * @see     [[Method]]`.isVirtualMethodDeclaration` for further details.
     * @note    The map only contains those methods which have at least one concrete
     *          implementation.
     */
    def overridingMethods(
        classHierarchy:        ClassHierarchy,
        virtualMethodsCount:   Int,
        objectTypeToClassFile: Map[ObjectType, ClassFile]
    )(
        implicit
        theLogContext: LogContext
    ): Map[Method, immutable.Set[Method]] = time {

        implicit val classFileRepository = new ClassFileRepository {
            override implicit def logContext: LogContext = theLogContext
            override def classFile(objectType: ObjectType): Option[ClassFile] = {
                objectTypeToClassFile.get(objectType)
            }
        }

        // IDEA
        // 0.   We start with the leaf nodes of the class hierarchy and store for each method
        //      the set of overriding methods (recall that the overrides relation is reflexive).
        //      Hence, initially the set of overriding methods for a method contains the method
        //      itself.
        //
        // 1.   After that the direct superclass is scheduled to be analyzed if all subclasses
        //      are analyzed. The superclass then tests for each overridable method if it is
        //      overridden in the subclasses and, if so, looks up the respective sets of overriding
        //      methods and joins them.
        //      A method is overridden by a subclass if the set of instance methods of the
        //      subclass does not contain the super class' method.
        //
        // 2.   Continue with 1.

        // Stores for each type the number of subtypes that still need to be processed.
        val subtypesToProcessCounts = new Array[Int](ObjectType.objectTypesCount)
        classHierarchy.foreachKnownType { objectType =>
            val oid = objectType.id
            subtypesToProcessCounts(oid) = classHierarchy.directSubtypesCount(oid)
        }

        val methods = new mutable.AnyRefMap[Method, immutable.Set[Method]](virtualMethodsCount)

        def computeOverridingMethods(tasks: Tasks[ObjectType], objectType: ObjectType): Unit = {
            val declaredMethodPackageName = objectType.packageName

            // If we don't know anything about the methods, we just do nothing;
            // instanceMethods will also just reuse the information derived from the superclasses.
            try {
                for {
                    cf <- objectTypeToClassFile.get(objectType)
                    declaredMethod <- cf.methods
                    if declaredMethod.isVirtualMethodDeclaration
                } {
                    if (declaredMethod.isFinal) { //... the method is necessarily not abstract...
                        methods += ((declaredMethod, immutable.Set(declaredMethod)))
                    } else {
                        var overridingMethods = immutable.Set.empty[Method]
                        // let's join the results of all subtypes
                        classHierarchy.foreachSubtypeCF(objectType) { subtypeClassFile =>
                            subtypeClassFile.findDirectlyOverridingMethod(
                                declaredMethodPackageName,
                                declaredMethod
                            ) match {
                                case _: NoResult => true
                                case Success(overridingMethod) =>
                                    val nextOverridingMethods = methods(overridingMethod)
                                    if (nextOverridingMethods.nonEmpty) {
                                        overridingMethods ++= nextOverridingMethods
                                    }
                                    false // we don't have to analyze subsequent subtypes.
                            }
                        }

                        if (declaredMethod.isNotAbstract) overridingMethods += declaredMethod

                        methods(declaredMethod) = overridingMethods
                    }
                }
            } finally {
                // The try-finally is a safety net to ensure that this method at least
                // terminates and that exceptions can be reported!
                classHierarchy.foreachDirectSupertype(objectType) { supertype =>
                    val sid = supertype.id
                    val newCount = subtypesToProcessCounts(sid) - 1
                    subtypesToProcessCounts(sid) = newCount
                    if (newCount == 0) {
                        tasks.submit(supertype)
                    }
                }
            }
        }

        val tasks = new SequentialTasks[ObjectType](computeOverridingMethods)
        classHierarchy.leafTypes foreach { t => tasks.submit(t) }
        try {
            tasks.join()
        } catch {
            case ce: ConcurrentExceptions =>
                error("project setup", "computing overriding methods failed, e")
                ce.getSuppressed foreach { e =>
                    error("project setup", "computing the overriding methods failed", e)
                }
        }
        methods.repack()
        methods
    } { t =>
        info("project setup", s"computing overriding information took ${t.toSeconds}")
    }

    //
    //
    // FACTORY METHODS
    //
    //

    /**
     * Given a reference to a class file, jar file or a folder containing jar and class
     * files, all class files will be loaded and a project will be returned.
     *
     * The global logger will be used for logging messages.
     */
    def apply(file: File): Project[URL] = {
        Project.apply(file, OPALLogger.globalLogger())
    }

    def apply(file: File, projectLogger: OPALLogger): Project[URL] = {
        apply(JavaClassFileReader().ClassFiles(file), projectLogger = projectLogger)
    }

    def apply(file: File, logContext: LogContext, config: Config): Project[URL] = {
        val reader = JavaClassFileReader(logContext, config)
        this(
            projectClassFilesWithSources = reader.ClassFiles(file),
            libraryClassFilesWithSources = Iterable.empty,
            libraryClassFilesAreInterfacesOnly = true,
            virtualClassFiles = Iterable.empty,
            handleInconsistentProject = defaultHandlerForInconsistentProjects,
            config = config,
            logContext
        )
    }

    def apply(
        projectFiles: Array[File],
        libraryFiles: Array[File],
        logContext:   LogContext,
        config:       Config
    ): Project[URL] = {
        this(
            JavaClassFileReader(logContext, config).AllClassFiles(projectFiles),
            JavaLibraryClassFileReader.AllClassFiles(libraryFiles),
            libraryClassFilesAreInterfacesOnly = true,
            virtualClassFiles = Iterable.empty,
            handleInconsistentProject = defaultHandlerForInconsistentProjects,
            config = config,
            logContext
        )
    }

    def apply[Source](
        projectClassFilesWithSources: Iterable[(ClassFile, Source)]
    ): Project[Source] = {
        Project.apply[Source](
            projectClassFilesWithSources,
            projectLogger = OPALLogger.globalLogger()
        )
    }

    def apply[Source](
        projectClassFilesWithSources: Iterable[(ClassFile, Source)],
        projectLogger:                OPALLogger
    ): Project[Source] = {
        Project.apply[Source](
            projectClassFilesWithSources,
            Iterable.empty,
            libraryClassFilesAreInterfacesOnly = false /*it actually doesn't matter*/ ,
            virtualClassFiles = Iterable.empty
        )(projectLogger = projectLogger)
    }

    def apply(
        projectFile: File,
        libraryFile: File
    ): Project[URL] = {
        implicit val logContext: LogContext = GlobalLogContext
        val libraries: Iterable[(ClassFile, URL)] =
            if (!libraryFile.exists) {
                OPALLogger.error("project configuration", s"$libraryFile does not exist")
                Iterable.empty
            } else {
                val libraries = JavaLibraryClassFileReader.ClassFiles(libraryFile)
                if (libraries.isEmpty)
                    OPALLogger.warn("project configuration", s"$libraryFile is empty")
                libraries
            }
        apply(
            JavaClassFileReader().ClassFiles(projectFile),
            libraries,
            libraryClassFilesAreInterfacesOnly = true,
            virtualClassFiles = Iterable.empty
        )
    }

    def apply(
        projectFiles: Array[File],
        libraryFiles: Array[File]
    ): Project[URL] = {
        apply(
            JavaClassFileReader().AllClassFiles(projectFiles),
            JavaLibraryClassFileReader.AllClassFiles(libraryFiles),
            libraryClassFilesAreInterfacesOnly = true,
            virtualClassFiles = Iterable.empty
        )
    }

    def extend(project: Project[URL], file: File): Project[URL] = {
        project.extend(JavaClassFileReader().ClassFiles(file))
    }

    /**
     * Creates a new `Project` that consists of the class files of the previous
     * project and the newly given class files.
     */
    def extend[Source](
        project:                      Project[Source],
        projectClassFilesWithSources: Iterable[(ClassFile, Source)]
    ): Project[Source] = {

        apply(
            project.projectClassFilesWithSources ++ projectClassFilesWithSources,
            // We cannot ensure that the newly provided class files are loaded in the same way
            // therefore, we do not support extending the set of library class files.
            project.libraryClassFilesWithSources,
            project.libraryClassFilesAreInterfacesOnly,
            virtualClassFiles = Iterable.empty
        )(config = project.config, projectLogger = OPALLogger.logger(project.logContext.successor))
    }

    /**
     * Creates a new `Project` that consists of the class files of the previous
     * project and the newly given class files.
     */
    private def extend[Source](
        project:                      Project[Source],
        projectClassFilesWithSources: Iterable[(ClassFile, Source)],
        libraryClassFilesWithSources: Iterable[(ClassFile, Source)]
    ): Project[Source] = {

        apply(
            project.projectClassFilesWithSources ++ projectClassFilesWithSources,
            project.libraryClassFilesWithSources ++ libraryClassFilesWithSources,
            project.libraryClassFilesAreInterfacesOnly,
            virtualClassFiles = Iterable.empty
        )(project.config, OPALLogger.logger(project.logContext.successor))
    }

    def apply[Source](
        projectClassFilesWithSources:       Iterable[(ClassFile, Source)],
        libraryClassFilesWithSources:       Iterable[(ClassFile, Source)],
        libraryClassFilesAreInterfacesOnly: Boolean
    ): Project[Source] = {
        Project.apply[Source](
            projectClassFilesWithSources,
            libraryClassFilesWithSources,
            libraryClassFilesAreInterfacesOnly,
            virtualClassFiles = Iterable.empty
        )
    }

    /**
     * Creates a new `Project` that consists of the source files of the previous
     * project and uses the (new) configuration. The old project
     * configuration is — by default – used as a fallback, so not all values have to be updated.
     *
     * If you just want to clear the derived data, using `Project.recreate` is more efficient.
     */
    def recreate[Source](
        project:                Project[Source],
        config:                 Config          = ConfigFactory.empty(),
        useOldConfigAsFallback: Boolean         = true
    ): Project[Source] = {
        apply(
            project.projectClassFilesWithSources,
            project.libraryClassFilesWithSources,
            project.libraryClassFilesAreInterfacesOnly,
            virtualClassFiles = Iterable.empty
        )(
                if (useOldConfigAsFallback) config.withFallback(project.config) else config,
                projectLogger = OPALLogger.logger(project.logContext.successor)
            )
    }

    /**
     * Creates a new Project.
     *
     * @param projectClassFilesWithSources The list of class files of this project that are considered
     *      to belong to the application/library that will be analyzed.
     *      [Thread Safety] The underlying data structure has to support concurrent access.
     *
     * @param libraryClassFilesWithSources The list of class files of this project that make up
     *      the libraries used by the project that will be analyzed.
     *      [Thread Safety] The underlying data structure has to support concurrent access.
     *
     * @param libraryClassFilesAreInterfacesOnly If `true` then only the non-private interface of
     *         of the classes belonging to the library was loaded. I.e., this setting just reflects
     *         the way how the class files were loaded; it does not change the classes!
     *
     * @param virtualClassFiles A list of virtual class files that have no direct
     *      representation in the project.
     *      Such declarations are created, e.g., to handle `invokedynamic`
     *      instructions.
     *      '''In general, such class files should be added using
     *      `projectClassFilesWithSources` and the `Source` should be the file that
     *      was the reason for the creation of this additional `ClassFile`.'''
     *      [Thread Safety] The underlying data structure has to support concurrent access.
     *
     * @param handleInconsistentProject A function that is called back if the project
     *      is not consistent. The default behavior
     *      ([[defaultHandlerForInconsistentProjects]]) is to write a warning
     *      message to the console. Alternatively it is possible to throw the given
     *      exception to cancel the loading of the project (which is the only
     *      meaningful option for several advanced analyses.)
     */
    def apply[Source](
        projectClassFilesWithSources:       Iterable[(ClassFile, Source)],
        libraryClassFilesWithSources:       Iterable[(ClassFile, Source)],
        libraryClassFilesAreInterfacesOnly: Boolean,
        virtualClassFiles:                  Iterable[ClassFile]           = Iterable.empty,
        handleInconsistentProject:          HandleInconsistentProject     = defaultHandlerForInconsistentProjects
    )(
        implicit
        config:        Config     = BaseConfig,
        projectLogger: OPALLogger = OPALLogger.globalLogger()
    ): Project[Source] = {
        implicit val logContext = new StandardLogContext()
        OPALLogger.register(logContext, projectLogger)
        this(
            projectClassFilesWithSources,
            libraryClassFilesWithSources,
            libraryClassFilesAreInterfacesOnly,
            virtualClassFiles,
            handleInconsistentProject,
            config,
            logContext
        )
    }

    def apply[Source](
        projectClassFilesWithSources:       Iterable[(ClassFile, Source)],
        libraryClassFilesWithSources:       Iterable[(ClassFile, Source)],
        libraryClassFilesAreInterfacesOnly: Boolean,
        virtualClassFiles:                  Iterable[ClassFile],
        handleInconsistentProject:          HandleInconsistentProject,
        config:                             Config,
        logContext:                         LogContext
    ): Project[Source] = time {
        implicit val projectConfig = config
        implicit val projectLogContext = logContext

        try {
            import scala.collection.mutable.Set
            import scala.concurrent.Await
            import scala.concurrent.Future
            import scala.concurrent.duration.Duration
            import scala.concurrent.ExecutionContext.Implicits.{global => ScalaExecutionContext}

            val classHierarchyFuture: Future[ClassHierarchy] = Future {
                time {
                    val OTObject = ObjectType.Object
                    val typeHierarchyDefinitions =
                        if (projectClassFilesWithSources.exists(_._1.thisType == OTObject) ||
                            libraryClassFilesWithSources.exists(_._1.thisType == OTObject)) {
                            info("project configuration", "the JDK is part of the analysis")
                            ClassHierarchy.noDefaultTypeHierarchyDefinitions()
                        } else {
                            val alternative =
                                "(using the preconfigured type hierarchy (based on Java 7) "+
                                    "for classes belonging java.lang)"
                            info("project configuration", "JDK classes not found "+alternative)
                            ClassHierarchy.defaultTypeHierarchyDefinitions()
                        }
                    ClassHierarchy(
                        projectClassFilesWithSources.view.map(_._1) ++
                            libraryClassFilesWithSources.view.map(_._1) ++
                            virtualClassFiles,
                        typeHierarchyDefinitions
                    )
                } { t =>
                    info("project setup", s"computing type hierarchy took ${t.toSeconds}")
                }
            }(ScalaExecutionContext)

            val projectModules = AnyRefMap.empty[String, ModuleDefinition[Source]]
            var projectClassFiles = List.empty[ClassFile]
            val projectTypes = Set.empty[ObjectType]
            var projectClassFilesCount: Int = 0
            var projectMethodsCount: Int = 0
            var projectFieldsCount: Int = 0

            val libraryModules = AnyRefMap.empty[String, ModuleDefinition[Source]]
            var libraryClassFiles = List.empty[ClassFile]
            var libraryClassFilesCount: Int = 0
            var libraryMethodsCount: Int = 0
            var libraryFieldsCount: Int = 0

            var codeSize: Long = 0L

            val objectTypeToClassFile = mutable.HashMap.empty[ObjectType, ClassFile] // IMPROVE Use ArrayMap as soon as we have project-local object type ids
            val sources = mutable.HashMap.empty[ObjectType, Source] // IMPROVE Use ArrayMap as soon as we have project-local object type ids
            val nests = mutable.HashMap.empty[ObjectType, ObjectType] // IMPROVE Use ArrayMap as soon as we have project-local object type ids

            def processModule(
                classFile:        ClassFile,
                source:           Option[Source],
                modulesContainer: AnyRefMap[String, ModuleDefinition[Source]]
            ): Unit = {
                val moduleName = classFile.module.get.name
                if (projectModules.contains(moduleName)) {
                    handleInconsistentProject(
                        logContext,
                        InconsistentProjectException(
                            s"the module $moduleName is defined as part of the project:\n\t"+
                                projectModules(moduleName).source.getOrElse("")+" and\n\t"+
                                source.map(_.toString).getOrElse("")+
                                "\n\tkeeping the first one."
                        )
                    )
                } else if (libraryModules.contains(moduleName)) {
                    handleInconsistentProject(
                        logContext,
                        InconsistentProjectException(
                            s"the module $moduleName is defined as part of the libraries:\n\t"+
                                libraryModules(moduleName).source.getOrElse("")+" and\n\t"+
                                source.map(_.toString).getOrElse("")+
                                "\n\tkeeping the first one."
                        )
                    )
                } else {
                    modulesContainer += ((moduleName, ModuleDefinition(classFile, source)))
                }
            }

            def processNestInformation(classFile: ClassFile, classType: ObjectType): Unit = {
                def putNestInfo(member: ObjectType, host: ObjectType): Unit = {
                    val prevHost = nests.put(member, host)
                    if (prevHost.isDefined && prevHost.get != host)
                        handleInconsistentProject(
                            logContext,
                            InconsistentProjectException(
                                s"inconsistent nesting information for class $member, "+
                                    s"found in nests $host and ${prevHost.get}"+
                                    "\n\tkeeping the first one."
                            )
                        )
                }

                classFile.attributes.foreach {
                    case NestHost(hostClassType) => putNestInfo(classType, hostClassType)
                    case NestMembers(classes) =>
                        putNestInfo(classType, classType)
                        classes.foreach(putNestInfo(_, classType))
                    case _ =>
                }
            }

            def processProjectClassFile(classFile: ClassFile, source: Option[Source]): Unit = {
                val projectType = classFile.thisType
                if (classFile.isModuleDeclaration) {
                    processModule(classFile, source, projectModules)
                } else if (projectTypes.contains(projectType)) {
                    handleInconsistentProject(
                        logContext,
                        InconsistentProjectException(
                            s"${projectType.toJava} is defined by multiple class files:\n\t"+
                                sources.get(projectType).getOrElse("")+" and\n\t"+
                                source.map(_.toString).getOrElse("")+
                                "\n\tkeeping the first one."
                        )
                    )
                } else {
                    projectTypes += projectType
                    projectClassFiles = classFile :: projectClassFiles
                    projectClassFilesCount += 1
                    for (method <- classFile.methods) {
                        projectMethodsCount += 1
                        method.body.foreach(codeSize += _.instructions.length)
                    }
                    projectFieldsCount += classFile.fields.size
                    objectTypeToClassFile(projectType) = classFile
                    source.foreach(sources(projectType) = _)
                    processNestInformation(classFile, projectType)
                }
            }

            for ((classFile, source) <- projectClassFilesWithSources) {
                processProjectClassFile(classFile, Some(source))
            }

            for (classFile <- virtualClassFiles) {
                processProjectClassFile(classFile, None)
            }

            // The set `libraryTypes` is only used to improve the identification of
            // inconsistent projects while loading libraries.
            val libraryTypes = Set.empty[ObjectType]
            for ((libClassFile, source) <- libraryClassFilesWithSources) {
                val libraryType = libClassFile.thisType

                if (libClassFile.isModuleDeclaration) {
                    processModule(libClassFile, Some(source), libraryModules)

                } else if (projectTypes.contains(libClassFile.thisType)) {
                    val libraryTypeQualifier =
                        if (libClassFile.isInterfaceDeclaration) "interface" else "class"
                    val projectTypeQualifier = {
                        val projectClassFile = projectClassFiles.find(_.thisType == libraryType).get
                        if (projectClassFile.isInterfaceDeclaration) "interface" else "class"
                    }

                    handleInconsistentProject(
                        logContext,
                        InconsistentProjectException(
                            s"${libraryType.toJava} is defined by the project and a library: "+
                                sources.getOrElse(libraryType, "")+" and "+
                                source.toString+"; keeping the project class file."
                        )
                    )

                    if (libraryTypeQualifier != projectTypeQualifier) {
                        handleInconsistentProject(
                            logContext,
                            InconsistentProjectException(
                                s"the kind of the type ${libraryType.toJava} "+
                                    s"defined by the project ($projectTypeQualifier) "+
                                    s"and a library ($libraryTypeQualifier) differs"
                            )
                        )
                    }

                } else if (libraryTypes.contains(libraryType)) {
                    handleInconsistentProject(
                        logContext,
                        InconsistentProjectException(
                            s"${libraryType.toJava} is defined multiple times in the libraries: "+
                                sources.getOrElse(libraryType, "")+" and "+
                                source.toString+"; keeping the first one."
                        )
                    )
                } else {
                    libraryClassFiles ::= libClassFile
                    libraryTypes += libraryType
                    libraryClassFilesCount += 1
                    for (method <- libClassFile.methods) {
                        libraryMethodsCount += 1
                        method.body.foreach(codeSize += _.instructions.length)
                    }
                    libraryFieldsCount += libClassFile.fields.size
                    objectTypeToClassFile(libraryType) = libClassFile
                    sources(libraryType) = source
                    processNestInformation(libClassFile, libraryType)
                }
            }

            val classHierarchy = Await.result(classHierarchyFuture, Duration.Inf)

            val instanceMethodsFuture = Future {
                this.instanceMethods(classHierarchy, objectTypeToClassFile.get)
            }

            val projectClassFilesArray = projectClassFiles.toArray
            val libraryClassFilesArray = libraryClassFiles.toArray
            val allMethods: Iterable[Method] = {
                new Iterable[Method] {
                    def iterator: Iterator[Method] = {
                        projectClassFilesArray.iterator.flatMap { cf => cf.methods } ++
                            libraryClassFilesArray.iterator.flatMap { cf => cf.methods }
                    }
                }
            }
            val virtualMethodsCount: Int = allMethods.count(m => m.isVirtualMethodDeclaration)

            val overridingMethodsFuture = Future {
                this.overridingMethods(classHierarchy, virtualMethodsCount, objectTypeToClassFile)
            }

            val methodsWithBodySortedBySizeWithContext =
                (projectClassFiles.iterator.flatMap(_.methods) ++
                    libraryClassFiles.iterator.flatMap(_.methods)).
                    filter(m => m.body.isDefined).
                    map(m => MethodInfo(sources(m.classFile.thisType), m)).
                    toArray.
                    sortWith { (v1, v2) => v1.method.body.get.codeSize > v2.method.body.get.codeSize }

            val methodsWithBodySortedBySize: Array[Method] =
                methodsWithBodySortedBySizeWithContext.map(mi => mi.method)

            val MethodHandleSubtypes = {
                classHierarchy.allSubtypes(ObjectType.MethodHandle, reflexive = true)
            }

            val VarHandleSubtypes = {
                classHierarchy.allSubtypes(ObjectType.VarHandle, reflexive = true)
            }

            val classFilesCount: Int = projectClassFilesCount + libraryClassFilesCount

            val methodsCount: Int = projectMethodsCount + libraryMethodsCount

            val fieldsCount: Int = projectFieldsCount + libraryFieldsCount

            val allProjectClassFiles: ArraySeq[ClassFile] = ArraySeq.unsafeWrapArray(projectClassFilesArray)

            val allLibraryClassFiles: ArraySeq[ClassFile] = ArraySeq.unsafeWrapArray(libraryClassFilesArray)

            val allClassFiles: Iterable[ClassFile] = {
                new Iterable[ClassFile] {
                    def iterator: Iterator[ClassFile] = {
                        projectClassFilesArray.iterator ++ libraryClassFilesArray.iterator
                    }
                }
            }

            val allFields: Iterable[Field] = {
                new Iterable[Field] {
                    def iterator: Iterator[Field] = {
                        projectClassFilesArray.iterator.flatMap { cf => cf.fields } ++
                            libraryClassFilesArray.iterator.flatMap { cf => cf.fields }
                    }
                }
            }

            val allSourceElements: Iterable[SourceElement] = allMethods ++ allFields ++ allClassFiles

            val project = new Project(
                projectModules,
                projectClassFilesArray,
                libraryModules,
                libraryClassFilesArray,
                libraryClassFilesAreInterfacesOnly,
                methodsWithBodySortedBySize,
                methodsWithBodySortedBySizeWithContext,
                projectTypes,
                objectTypeToClassFile,
                sources,
                projectClassFilesCount,
                projectMethodsCount,
                projectFieldsCount,
                libraryClassFilesCount,
                libraryMethodsCount,
                libraryFieldsCount,
                codeSize,
                MethodHandleSubtypes,
                VarHandleSubtypes,
                classFilesCount,
                methodsCount,
                fieldsCount,
                allProjectClassFiles,
                allLibraryClassFiles,
                allClassFiles,
                allMethods,
                allFields,
                allSourceElements,
                virtualMethodsCount,
                classHierarchy,
                Await.result(instanceMethodsFuture, Duration.Inf),
                Await.result(overridingMethodsFuture, Duration.Inf),
                nests
            )

            time {
                val issues = validate(project)
                issues.foreach { handleInconsistentProject(logContext, _) }
                info(
                    "project configuration",
                    s"project validation revealed ${issues.size} significant issues"+
                        (
                            if (issues.nonEmpty)
                                "; validate the configured libraries for inconsistencies"
                            else
                                ""
                        )
                )
            } { t => info("project setup", s"validating the project took ${t.toSeconds}") }

            project
        } catch {
            case t: Throwable => OPALLogger.unregister(logContext); throw t
        }
    } { t =>
        // If an exception was thrown, the logContext is no longer available!
        val lc = if (OPALLogger.isUnregistered(logContext)) GlobalLogContext else logContext
        info("project setup", s"creating the project took ${t.toSeconds}")(lc)
    }

}

case class ModuleDefinition[Source](module: ClassFile, source: Option[Source])




© 2015 - 2025 Weber Informatics LLC | Privacy Policy