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

org.opalj.br.fpcf.analyses.immutability.ClassImmutabilityAnalysis.scala Maven / Gradle / Ivy

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

import scala.collection.immutable.SortedSet

import org.opalj.br.ClassFile
import org.opalj.br.ClassSignature
import org.opalj.br.ClassTypeSignature
import org.opalj.br.FormalTypeParameter
import org.opalj.br.ObjectType
import org.opalj.br.SimpleClassTypeSignature
import org.opalj.br.analyses.ProjectInformationKeys
import org.opalj.br.analyses.SomeProject
import org.opalj.br.fpcf.FPCFAnalysis
import org.opalj.br.fpcf.FPCFAnalysisScheduler
import org.opalj.br.fpcf.FPCFEagerAnalysisScheduler
import org.opalj.br.fpcf.FPCFLazyAnalysisScheduler
import org.opalj.br.fpcf.properties.immutability.ClassImmutability
import org.opalj.br.fpcf.properties.immutability.DependentlyImmutableClass
import org.opalj.br.fpcf.properties.immutability.DependentlyImmutableField
import org.opalj.br.fpcf.properties.immutability.FieldImmutability
import org.opalj.br.fpcf.properties.immutability.MutableClass
import org.opalj.br.fpcf.properties.immutability.MutableField
import org.opalj.br.fpcf.properties.immutability.NonTransitivelyImmutableClass
import org.opalj.br.fpcf.properties.immutability.NonTransitivelyImmutableField
import org.opalj.br.fpcf.properties.immutability.TransitivelyImmutableClass
import org.opalj.br.fpcf.properties.immutability.TransitivelyImmutableField
import org.opalj.fpcf.ELBP
import org.opalj.fpcf.EOptionP
import org.opalj.fpcf.EPK
import org.opalj.fpcf.EPS
import org.opalj.fpcf.Entity
import org.opalj.fpcf.FinalEP
import org.opalj.fpcf.FinalP
import org.opalj.fpcf.IncrementalResult
import org.opalj.fpcf.InterimE
import org.opalj.fpcf.InterimResult
import org.opalj.fpcf.LBP
import org.opalj.fpcf.LUBP
import org.opalj.fpcf.MultiResult
import org.opalj.fpcf.ProperPropertyComputationResult
import org.opalj.fpcf.Property
import org.opalj.fpcf.PropertyBounds
import org.opalj.fpcf.PropertyComputation
import org.opalj.fpcf.PropertyStore
import org.opalj.fpcf.Result
import org.opalj.fpcf.Results
import org.opalj.fpcf.SomeEPS
import org.opalj.fpcf.UBP
import org.opalj.log.LogContext
import org.opalj.log.OPALLogger

/**
 *
 * Determines the immutability of instances of a specific class. In the case of an abstract class
 * the (implicit) assumption is made that all abstract methods (if any) are/can
 * be implemented without necessarily/always requiring additional state; i.e., only the currently
 * defined fields are taken into consideration. An interfaces is always considered to be transitively immutable.
 * If you need to know if all possible instances of an interface or some type; i.e., all instances
 * of the classes that implement the respective interface/inherit from some class are transitively immutable,
 * you can query the [[TypeImmutability]] property.
 *
 * In case of incomplete class hierarchies or if the class hierarchy is complete, but some
 * class files are not found the sound approximation is done that the respective classes are
 * mutable.
 *
 * This analysis uses the [[FieldImmutability]] property to determine the immutability of a class.
 *
 * TODO Discuss the case if a constructor calls an instance method which is overrideable (See Verifiable Functional Purity Paper for some arguments.)
 *
 * @author Michael Eichberg
 * @author Florian Kübler
 * @author Dominik Helm
 * @author Tobias Roth
 *
 */
class ClassImmutabilityAnalysis(val project: SomeProject) extends FPCFAnalysis {
    /*
   * The analysis is implemented as an incremental analysis which starts with the analysis
   * of those types which directly inherit from java.lang.Object and then propagates the
   * immutability information down the class hierarchy.
   *
   * This propagation needs to be done eagerly to ensure that all types are associated with
   * some property when the initial computation finishes and fallback properties are associated.
   */

    /**
     * Creates a result object that sets this type and all subclasses of if to the given immutability rating.
     */
    @inline private[this] def createResultForAllSubtypes(
        t:            ObjectType,
        immutability: ClassImmutability
    ): MultiResult = {
        val allSubtypes = classHierarchy.allSubclassTypes(t, reflexive = true)
        val r = allSubtypes.map { st => new FinalEP(st, immutability) }.toSeq
        MultiResult(r)
    }
    @inline private[this] def createIncrementalResult(
        t:                     ObjectType,
        cfImmutability:        EOptionP[Entity, Property],
        cfImmutabilityIsFinal: Boolean,
        result:                ProperPropertyComputationResult
    ): IncrementalResult[ClassFile] = {
        var results: List[ProperPropertyComputationResult] = List(result)
        var nextComputations: List[(PropertyComputation[ClassFile], ClassFile)] = Nil
        val directSubtypes = classHierarchy.directSubtypesOf(t)
        directSubtypes.foreach { t =>
            project.classFile(t) match {
                case Some(scf) =>
                    nextComputations ::= (
                        (
                            determineClassImmutability(t, cfImmutability, cfImmutabilityIsFinal,
                                lazyComputation = false), scf
                        )
                    )
                case None =>
                    OPALLogger.warn(
                        "project configuration - class immutability analysis",
                        s"missing class file of ${t.toJava}; setting all subtypes to mutable"
                    )
                    results ::= createResultForAllSubtypes(t, MutableClass)
            }
        }
        IncrementalResult(Results(results), nextComputations.iterator)
    }

    def determineGenericTypeBounds(classFile: ClassFile): Set[(String, String)] = {
        var genericTypeBounds: Set[(String, String)] = Set.empty
        classFile.attributes.toList.collectFirst({
            case ClassSignature(typeParameters, _, _) => typeParameters.collect({
                case ftp @ FormalTypeParameter(_, _, _) => ftp
            })
                .foreach {
                    case FormalTypeParameter(identifier, classBound, _) => classBound match {

                        case Some(ClassTypeSignature(_, SimpleClassTypeSignature(simpleName, _), _)) =>
                            genericTypeBounds += ((identifier, simpleName))

                        case _ =>
                    }

                }
        })
        genericTypeBounds
    }

    /*
     * If the type is transitively immutable the class itself is also transitively immutable.
     */
    val defaultTransitivelyImmutableTypes = project.config.getStringList(
        "org.opalj.fpcf.analyses.TypeImmutabilityAnalysis.defaultTransitivelyImmutableTypes"
    ).toArray().toList.map(s => ObjectType(s.asInstanceOf[String])).toSet

    def doDetermineClassImmutability(e: Entity): ProperPropertyComputationResult = {
        e match {
            case t: ObjectType =>
                if (defaultTransitivelyImmutableTypes.contains(t.asObjectType))
                    return Result(t, TransitivelyImmutableClass)
                //this is safe
                classHierarchy.superclassType(t) match {
                    case None => Result(t, MutableClass);
                    case Some(superClassType) =>
                        val cf = project.classFile(t) match {
                            case None =>
                                return Result(t, MutableClass); //TODO consider other lattice element
                            case Some(cf) => cf
                        }

                        propertyStore(superClassType, ClassImmutability.key) match {
                            case UBP(MutableClass) =>
                                Result(t, MutableClass)
                            case eps: EPS[ObjectType, ClassImmutability] =>
                                determineClassImmutability(
                                    superClassType,
                                    eps,
                                    eps.isFinal,
                                    lazyComputation = true
                                )(cf)
                            case epk =>
                                determineClassImmutability(
                                    superClassType,
                                    epk,
                                    superClassImmutabilityIsFinal = false,
                                    lazyComputation = true
                                )(cf)
                        }
                }
            case _ =>
                val m = e.getClass.getSimpleName+" is not an org.opalj.br.ObjectType"
                throw new IllegalArgumentException(m)
        }
    }

    private[this] object SuperClassKey

    /**
     * Determines the immutability of instances of the given class type `t`.
     *
     * @param superClassType The direct super class of the given object type `t`.
     *      Can be `null` if `superClassImmutability` is `TransitivelyImmutable`.
     * @param superClassInformation The mutability of the given super class. The mutability
     *      must not be "MutableObject"; this case has to be handled explicitly. Hence,
     *      the mutability is either unknown, immutable or (at least) conditionally immutable.
     */
    def determineClassImmutability(
        superClassType:                ObjectType,
        superClassInformation:         EOptionP[Entity, Property],
        superClassImmutabilityIsFinal: Boolean,
        lazyComputation:               Boolean
    )(
        cf: ClassFile
    ): ProperPropertyComputationResult = {
        val t = cf.thisType

        var dependees = Map.empty[Entity, EOptionP[Entity, Property]]

        if (!superClassImmutabilityIsFinal) {
            dependees += (SuperClassKey -> superClassInformation)
        }

        // Collect all fields for which we need to determine the effective immutability!
        var hasFieldsWithUnknownImmutability = false

        val instanceFields = cf.fields.iterator.filter { f =>
            !f.isStatic
        }.toList
        var hasNonTransitivelyImmutableFields = false
        var hasDependentlyImmutableFields = false
        var genericTypeParameters: SortedSet[String] = SortedSet.empty

        val fieldsPropertyStoreInformation = propertyStore(instanceFields, FieldImmutability)

        fieldsPropertyStoreInformation.foreach {

            case FinalP(MutableField) =>
                if (lazyComputation)
                    return Result(t, MutableClass);
                else
                    return createResultForAllSubtypes(t, MutableClass);

            case FinalP(NonTransitivelyImmutableField) => hasNonTransitivelyImmutableFields = true

            case FinalP(DependentlyImmutableField(parameters)) =>
                genericTypeParameters ++= parameters
                hasDependentlyImmutableFields = true

            case FinalP(TransitivelyImmutableField) =>

            case ep @ InterimE(e) =>
                hasFieldsWithUnknownImmutability = true
                dependees += (e -> ep)

            case epk @ EPK(e: Entity, _) =>
                // <=> The immutability information is not yet available.
                hasFieldsWithUnknownImmutability = true
                dependees += (e -> epk)

            case _ =>
                if (lazyComputation) //TODO check
                    return Result(t, MutableClass);
                else
                    return createResultForAllSubtypes(t, MutableClass);
        }

        var minLocalImmutability: ClassImmutability = MutableClass

        // NOTE: maxLocalImmutability does not take the super classes' mutability into account!
        var maxLocalImmutability: ClassImmutability = superClassInformation match {
            case UBP(MutableClass)                  => MutableClass
            case UBP(NonTransitivelyImmutableClass) => NonTransitivelyImmutableClass
            case UBP(DependentlyImmutableClass(parameters)) =>
                genericTypeParameters ++= parameters
                DependentlyImmutableClass(genericTypeParameters)
            case _ => TransitivelyImmutableClass
        }
        if (hasNonTransitivelyImmutableFields) {
            maxLocalImmutability = NonTransitivelyImmutableClass
        }

        if (hasDependentlyImmutableFields &&
            maxLocalImmutability != NonTransitivelyImmutableClass && maxLocalImmutability != MutableClass) {
            maxLocalImmutability = DependentlyImmutableClass(genericTypeParameters)
        }

        if (cf.fields.exists(f => !f.isStatic && f.fieldType.isArrayType)) {
            // IMPROVE We could analyze if the array is effectively final.
            // I.e., it is only initialized once (at construction time) and no reference to it
            // is passed to another object.
            maxLocalImmutability = NonTransitivelyImmutableClass
        }

        if (dependees.isEmpty || minLocalImmutability == maxLocalImmutability) {
            // <=> the super classes' immutability is final
            //     (i.e., ImmutableObject or ImmutableContainer)
            // <=> all fields are (effectively) final
            // <=> the type mutability of all fields is final
            //     (i.e., ImmutableType or ImmutableContainerType)
            if (lazyComputation)
                return Result(t, maxLocalImmutability);

            return createIncrementalResult(
                t,
                FinalEP(t, maxLocalImmutability),
                cfImmutabilityIsFinal = true,
                Result(t, maxLocalImmutability)
            );
        }

        def c(someEPS: SomeEPS): ProperPropertyComputationResult = {
            //[DEBUG]
            //val oldDependees = dependees
            dependees = dependees.iterator.filter(_._1 ne someEPS.e).toMap
            someEPS match {
                // Superclass related dependencies:
                //
                case UBP(MutableClass) =>
                    return Result(t, MutableClass);

                case LBP(TransitivelyImmutableClass) => // the super class
                    dependees -= SuperClassKey

                case UBP(NonTransitivelyImmutableClass) => // super class is at most immutable container
                    if (someEPS.isFinal) dependees -= SuperClassKey
                    maxLocalImmutability = NonTransitivelyImmutableClass

                case UBP(DependentlyImmutableClass(parameter)) =>
                    if (someEPS.isFinal) dependees -= SuperClassKey
                    if (maxLocalImmutability != NonTransitivelyImmutableClass) {
                        genericTypeParameters ++= parameter
                        maxLocalImmutability = DependentlyImmutableClass(genericTypeParameters)
                    }

                case LBP(NonTransitivelyImmutableClass) => // super class is at least non-transitively immutable
                    if (minLocalImmutability != NonTransitivelyImmutableClass &&
                        !dependees.valuesIterator.exists(_.pk == FieldImmutability.key))
                        minLocalImmutability = NonTransitivelyImmutableClass // Lift lower bound when possible

                case LUBP(MutableClass, TransitivelyImmutableClass) => // No information about superclass

                case FinalP(DependentlyImmutableField(parameter)) =>
                    if (hasNonTransitivelyImmutableFields) {
                        maxLocalImmutability = NonTransitivelyImmutableClass
                    } else if (maxLocalImmutability != MutableClass &&
                        maxLocalImmutability != NonTransitivelyImmutableClass) {
                        genericTypeParameters ++= parameter
                        maxLocalImmutability = DependentlyImmutableClass(genericTypeParameters)
                    }

                // Field Immutability related dependencies:
                case FinalP(TransitivelyImmutableField) =>
                case FinalP(NonTransitivelyImmutableField) =>
                    maxLocalImmutability = NonTransitivelyImmutableClass
                case FinalP(MutableField) => return Result(t, MutableClass);
                case UBP(MutableField)    => return Result(t, MutableClass);
                case ELBP(e, NonTransitivelyImmutableField |
                    TransitivelyImmutableField) => dependees -= e
                case UBP(TransitivelyImmutableField)    => // no information about field mutability
                case UBP(NonTransitivelyImmutableField) => maxLocalImmutability = NonTransitivelyImmutableClass
                case UBP(DependentlyImmutableField(parameter)) if maxLocalImmutability != NonTransitivelyImmutableClass =>
                    genericTypeParameters ++= parameter
                    maxLocalImmutability =
                        DependentlyImmutableClass(genericTypeParameters)
                case _ => Result(t, MutableClass) //TODO
            }

            if (someEPS.isRefinable) {
                val entity = if (someEPS.pk == ClassImmutability.key) SuperClassKey else someEPS.e
                dependees += (entity -> someEPS)
            }

            /*[DEBUG]
                assert(
                    oldDependees != dependees,
                    s"dependees are not correctly updated $e($p)\n:old=$oldDependees\nnew=$dependees"
                )
       */

            if (dependees.isEmpty || minLocalImmutability == maxLocalImmutability) {
                Result(t, maxLocalImmutability)
            } else {
                InterimResult(t, minLocalImmutability, maxLocalImmutability, dependees.values.toSet, c)
            }
        }

        val result =
            InterimResult(t, minLocalImmutability, maxLocalImmutability, dependees.values.toSet, c)
        if (lazyComputation)
            result
        else {
            val isFinal = dependees.isEmpty
            createIncrementalResult(
                t,
                EPS(t, minLocalImmutability, maxLocalImmutability),
                isFinal,
                result
            )
        }
    }
}

trait ClassImmutabilityAnalysisScheduler extends FPCFAnalysisScheduler {

    final def derivedProperty: PropertyBounds = PropertyBounds.lub(ClassImmutability)

    final override def uses: Set[PropertyBounds] =
        PropertyBounds.lubs(ClassImmutability, FieldImmutability)

    override def requiredProjectInformation: ProjectInformationKeys = Seq()

    override type InitializationData = IterableOnce[ClassFile]

    private[this] def setResultsAndComputeEntities(
        project:       SomeProject,
        propertyStore: PropertyStore
    ): IterableOnce[ClassFile] = {
        val classHierarchy = project.classHierarchy
        import classHierarchy.allSubtypes
        import classHierarchy.rootClassTypesIterator
        import propertyStore.set
        implicit val logContext: LogContext = project.logContext

        // 1.1
        // java.lang.Object is by definition transitively immutable.
        set(ObjectType.Object, TransitivelyImmutableClass)

        // 1.2
        // All (instances of) interfaces are (by their very definition) also transitively immutable.
        val allInterfaces = project.allClassFiles.filter(cf => cf.isInterfaceDeclaration)
        allInterfaces.foreach(cf => set(cf.thisType, TransitivelyImmutableClass))

        // 2.
        // All classes that do not have complete superclass information are mutable
        // due to the lack of knowledge.
        // But, for classes that directly inherit from Object, but which also
        // implement unknown interface types it is possible to compute the class
        // immutability
        val unexpectedRootClassTypes = rootClassTypesIterator.filter(rt => rt ne ObjectType.Object)

        unexpectedRootClassTypes foreach { rt =>
            allSubtypes(rt, reflexive = true) foreach { ot =>
                project.classFile(ot) foreach { cf =>
                    set(cf.thisType, MutableClass)
                }
            }
        }

        // 3.
        // Compute the initial set of classes for which we want to determine the mutability.
        var cfs: List[ClassFile] = Nil
        classHierarchy
            .directSubclassesOf(ObjectType.Object)
            .iterator
            .map(ot => (ot, project.classFile(ot)))
            .foreach {
                case (_, Some(cf)) => cfs ::= cf
                case (t, None) =>
                    // This handles the case where the class hierarchy is at least partially
                    // based on a pre-configured class hierarchy (*.ths file).
                    // E.g., imagine that you analyze a lib which contains a class that inherits
                    // from java.lang.Exception, but you have no knowledge about this respective
                    // class...
                    OPALLogger.warn(
                        "project configuration - object immutability analysis",
                        s"${t.toJava}'s class file is not available"
                    )
                    allSubtypes(t, reflexive = true).foreach(project.classFile(_).foreach { cf =>
                        set(cf.thisType, MutableClass)
                    })
            }
        cfs
    }

    override def init(p: SomeProject, ps: PropertyStore): InitializationData = {
        setResultsAndComputeEntities(p, ps)
    }

    override def beforeSchedule(p: SomeProject, ps: PropertyStore): Unit = {}

    override def afterPhaseScheduling(ps: PropertyStore, analysis: FPCFAnalysis): Unit = {}

    override def afterPhaseCompletion(
        p:        SomeProject,
        ps:       PropertyStore,
        analysis: FPCFAnalysis
    ): Unit = {}
}

/**
 * Scheduler to run the class immutability analysis eagerly.
 * @author Tobias Roth
 * @author Michael Eichberg
 */
object EagerClassImmutabilityAnalysis extends ClassImmutabilityAnalysisScheduler
    with FPCFEagerAnalysisScheduler {

    override def derivesEagerly: Set[PropertyBounds] = Set(derivedProperty)

    override def derivesCollaboratively: Set[PropertyBounds] = Set.empty

    override def start(p: SomeProject, ps: PropertyStore, cfs: InitializationData): FPCFAnalysis = {
        val analysis = new ClassImmutabilityAnalysis(p)
        ps.scheduleEagerComputationsForEntities(cfs)(
            analysis.determineClassImmutability(
                superClassType = null,
                FinalEP(ObjectType.Object, TransitivelyImmutableClass),
                superClassImmutabilityIsFinal = true,
                lazyComputation = false
            )
        )
        analysis
    }
}

/**
 * Scheduler to run the class immutability analysis lazily.
 * @author Michael Eichberg
 */
object LazyClassImmutabilityAnalysis extends ClassImmutabilityAnalysisScheduler
    with FPCFLazyAnalysisScheduler {

    override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty)

    override def register(
        p:      SomeProject,
        ps:     PropertyStore,
        unused: InitializationData
    ): FPCFAnalysis = {
        val analysis = new ClassImmutabilityAnalysis(p)
        ps.registerLazyPropertyComputation(
            ClassImmutability.key,
            analysis.doDetermineClassImmutability
        )
        analysis
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy