
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