From 71a8486674b6edf506e545d75a2ee0965c3490ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 22 Oct 2025 14:10:32 +0200 Subject: [PATCH 01/41] Rework field assignability --- .../opalj/support/info/PureVoidMethods.scala | 3 +- .../scala/org/opalj/support/info/Purity.scala | 5 +- .../opalj/support/info/UnusedResults.scala | 3 +- .../InitializationInConstructor.java | 4 +- .../LazyInitializationPrimitiveTypes.java | 29 +- .../scala_lazy_val/LazyCell.java | 52 +- .../opalj/fpcf/FieldAssignabilityTests.scala | 62 +- .../scala/org/opalj/fpcf/PurityTests.scala | 3 +- .../immutability/FieldAssignability.scala | 6 +- .../tac/fpcf/analyses/L1PuritySmokeTest.scala | 3 +- .../src/main/scala/org/opalj/tac/Stmt.scala | 2 + .../AbstractFieldAssignabilityAnalysis.scala | 493 ++++---- .../L0FieldAssignabilityAnalysis.scala | 244 +--- .../L1FieldAssignabilityAnalysis.scala | 110 -- .../L2FieldAssignabilityAnalysis.scala | 1124 +++-------------- .../LazyInitializationAnalysis.scala | 802 ++++++++++++ 16 files changed, 1349 insertions(+), 1596 deletions(-) delete mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala index 59fc866639..bd31364a0d 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala @@ -25,7 +25,6 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL2PurityAnalysis /** @@ -60,7 +59,7 @@ object PureVoidMethods extends ProjectsAnalysisApplication { LazyInterProceduralEscapeAnalysis, LazyReturnValueFreshnessAnalysis, LazyFieldLocalityAnalysis, - LazyL1FieldAssignabilityAnalysis, + // TODO reincorporate LazyL1FieldAssignabilityAnalysis, LazyClassImmutabilityAnalysis, LazyTypeImmutabilityAnalysis, EagerL2PurityAnalysis diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala index 5f4d189d53..0557484184 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala @@ -59,7 +59,6 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL0FieldAssignabilityAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.L1PurityAnalysis import org.opalj.tac.fpcf.analyses.purity.L2PurityAnalysis import org.opalj.tac.fpcf.analyses.purity.LazyL1PurityAnalysis @@ -136,8 +135,8 @@ object Purity extends ProjectsAnalysisApplication { case Some(fA) => support ::= getScheduler(fA, eager) case None => analysis match { case LazyL0PurityAnalysis => support ::= LazyL0FieldAssignabilityAnalysis - case LazyL1PurityAnalysis => support ::= LazyL1FieldAssignabilityAnalysis - case LazyL2PurityAnalysis => support ::= LazyL1FieldAssignabilityAnalysis + case LazyL1PurityAnalysis => support ::= LazyL0FieldAssignabilityAnalysis // TODO find LazyL1FieldAssignabilityAnalysis + case LazyL2PurityAnalysis => support ::= LazyL0FieldAssignabilityAnalysis // TODO find LazyL1FieldAssignabilityAnalysis } } diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala index 6cc28f77c6..75c6de5d71 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala @@ -40,7 +40,6 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL2PurityAnalysis import org.opalj.tac.fpcf.properties.TACAI import org.opalj.value.ValueInformation @@ -83,7 +82,7 @@ object UnusedResults extends ProjectsAnalysisApplication { LazyInterProceduralEscapeAnalysis, LazyReturnValueFreshnessAnalysis, LazyFieldLocalityAnalysis, - LazyL1FieldAssignabilityAnalysis, + // TODO find LazyL1FieldAssignabilityAnalysis, LazyClassImmutabilityAnalysis, LazyTypeImmutabilityAnalysis, EagerL2PurityAnalysis diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/InitializationInConstructor.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/InitializationInConstructor.java index 1948156c92..771e368ea9 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/InitializationInConstructor.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/InitializationInConstructor.java @@ -11,15 +11,17 @@ class InitializationInConstructorAssignable { @AssignableField("The field is written everytime it is passed to the constructor of another instance.") private InitializationInConstructorAssignable child; + public InitializationInConstructorAssignable(InitializationInConstructorAssignable parent) { parent.child = this; - } } +} class InitializationInConstructorNonAssignable { @EffectivelyNonAssignableField("The field is only assigned once in its own constructor.") private InitializationInConstructorNonAssignable parent; + public InitializationInConstructorNonAssignable(InitializationInConstructorNonAssignable parent) { this.parent = parent.parent; } diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java index a407ee6593..c32fd15f7d 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java @@ -17,10 +17,10 @@ class Simple { @TransitivelyImmutableField(value = "field is lazily initialized and has primitive value", analyses = {}) - @MutableField(value = "The field is unsafely lazily initialized", analyses = { FieldImmutabilityAnalysis.class}) + @MutableField(value = "The field is unsafely lazily initialized", analyses = { FieldImmutabilityAnalysis.class }) @LazilyInitializedField(value = "Simple lazy initialization with primitive type", analyses = {}) - @UnsafelyLazilyInitializedField(value = "The analysis does not reconize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + @UnsafelyLazilyInitializedField(value = "The analysis does not recognize determinism", + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -35,8 +35,8 @@ class Local { @TransitivelyImmutableField(value = "field is lazily initialized and has primitive value", analyses = {}) @LazilyInitializedField(value = "Lazy initialization with local", analyses = {}) - @UnsafelyLazilyInitializedField(value = "The analysis does not reconize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + @UnsafelyLazilyInitializedField(value = "The analysis does not recognize determinism", + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -67,8 +67,8 @@ class LocalReversed { @TransitivelyImmutableField(value = "field is lazily initialized and has primitive value", analyses = {}) @LazilyInitializedField(value = "Lazy initialization with local (reversed)", analyses = {}) - @UnsafelyLazilyInitializedField(value = "The analysis does not reconize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + @UnsafelyLazilyInitializedField(value = "The analysis does not recognize determinism", + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -84,8 +84,8 @@ class SimpleReversed { @TransitivelyImmutableField(value = "field is lazily initialized and has primitive value", analyses = {}) @LazilyInitializedField(value = "Simple lazy initialization (reversed)", analyses = {}) - @UnsafelyLazilyInitializedField(value = "The analysis cannot reconizes determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + @UnsafelyLazilyInitializedField(value = "The analysis cannot recognize determinism", + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -123,7 +123,7 @@ class DeterministicCall { @LazilyInitializedField(value = "Lazy initialization with call to deterministic method", analyses = {}) @MutableField("field is unsafely lazily initialized and has primitive value") @UnsafelyLazilyInitializedField(value = "The analysis does not recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -161,7 +161,7 @@ class DeterministicCallOnFinalField { @TransitivelyImmutableField(value = "Field is lazily initialized and has primitive value", analyses = {}) @LazilyInitializedField(value = "Lazy initialization with call to deterministic method ", analyses = {}) @UnsafelyLazilyInitializedField(value = "The analysis does not recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; @NonAssignableField(value = "Declared final field") @@ -173,8 +173,7 @@ public DeterministicCallOnFinalField(int v) { private final class Inner { - @NonAssignableField("field is final") - final int val; + @NonAssignableField("field is final") final int val; public Inner(int v) { val = v; @@ -246,7 +245,7 @@ class DoubleLocalAssignment { @LazilyInitializedField(value = "Lazy initialization with a local that is updated twice with deterministic value", analyses = {}) @UnsafelyLazilyInitializedField(value = "The analysis does not recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -263,7 +262,7 @@ class DoubleAssignment { @UnsafelyLazilyInitializedField(value = "Field can be observed partially updated in single thread ", analyses = {}) @AssignableField(value = "The analysis does not recognize multiple but inconsequential writes", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java index 3f2ee05774..c67d58f36e 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java @@ -6,41 +6,39 @@ import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.LazilyInitializedField; import org.opalj.fpcf.properties.immutability.fields.TransitivelyImmutableField; -import org.opalj.tac.fpcf.analyses.fieldassignability.L1FieldAssignabilityAnalysis; import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; /** -* This class represents the implementation of a Scala lazy val from Scala 2.12. -* https://docs.scala-lang.org/sips/improved-lazy-val-initialization.html -* -*/ + * This class represents the implementation of a Scala lazy val from Scala 2.12. + * https://docs.scala-lang.org/scala3/reference/changed-features/lazy-vals-init.html + * + */ @TransitivelyImmutableClass(value = "The class has only transitive immutable fields", analyses = {}) public class LazyCell { -@TransitivelyImmutableField(value = "The field is lazily initialized and has a primitive type", analyses = {}) -@LazilyInitializedField(value = "The field is only set once in a synchronized way.", analyses = {}) -@AssignableField(value = "The analyses cannot recognize lazy initialization over multiple methods", - analyses = {L0FieldAssignabilityAnalysis.class, L1FieldAssignabilityAnalysis.class, - L2FieldAssignabilityAnalysis.class}) -private volatile boolean bitmap_0 = false; + @TransitivelyImmutableField(value = "The field is lazily initialized and has a primitive type", analyses = {}) + @LazilyInitializedField(value = "The field is only set once in a synchronized way.", analyses = {}) + @AssignableField(value = "The analyses cannot recognize lazy initialization over multiple methods", + analyses = { L0FieldAssignabilityAnalysis.class, L2FieldAssignabilityAnalysis.class }) + private volatile boolean bitmap_0 = false; -@TransitivelyImmutableField(value = "The field is lazily initialized and has a primitive type", analyses = {}) -@LazilyInitializedField(value = "The field is only set once in a synchronized way.", analyses = {}) -@AssignableField(value = "The analysis cannot recognize lazy initialization over multiple methods", - analyses = {L0FieldAssignabilityAnalysis.class, L1FieldAssignabilityAnalysis.class, - L2FieldAssignabilityAnalysis.class}) -Integer value_0; + @TransitivelyImmutableField(value = "The field is lazily initialized and has a primitive type", analyses = {}) + @LazilyInitializedField(value = "The field is only set once in a synchronized way.", analyses = {}) + @AssignableField(value = "The analysis cannot recognize lazy initialization over multiple methods", + analyses = { L0FieldAssignabilityAnalysis.class, L2FieldAssignabilityAnalysis.class }) + Integer value_0; -private Integer value_lzycompute() { - synchronized (this){ - if(value_0==0) { - value_0 = 42; - bitmap_0 = true; + private Integer value_lazy_compute() { + synchronized (this) { + if (value_0 == 0) { + value_0 = 42; + bitmap_0 = true; + } } + return value_0; + } + + public Integer getValue() { + return bitmap_0 ? value_0 : value_lazy_compute(); } - return value_0; -} -public Integer getValue(){ - return bitmap_0 ? value_0 : value_lzycompute(); -} } diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala index 1006d70166..14d14d30e1 100644 --- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala @@ -14,8 +14,6 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL0FieldAssignabilityAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL2FieldAssignabilityAnalysis /** @@ -34,14 +32,12 @@ class FieldAssignabilityTests extends PropertiesTest { override def createConfig(): Config = ConfigFactory.load("LibraryProject.conf") override def init(p: Project[URL]): Unit = { - p.updateProjectInformationKeyInitializationData(AIDomainFactoryKey) { _ => import org.opalj.ai.domain.l1 Set[Class[? <: AnyRef]](classOf[l1.DefaultDomainWithCFGAndDefUse[URL]]) } p.get(RTACallGraphKey) - } describe("no analysis is scheduled") { @@ -50,35 +46,35 @@ class FieldAssignabilityTests extends PropertiesTest { validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) } - describe("the org.opalj.fpcf.analyses.L0FieldAssignability is executed") { - - val as = executeAnalyses( - Set( - EagerL0FieldAssignabilityAnalysis, - LazyInterProceduralEscapeAnalysis, - LazyReturnValueFreshnessAnalysis, - LazyFieldLocalityAnalysis, - EagerFieldAccessInformationAnalysis - ) - ) - as.propertyStore.shutdown() - validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) - } - - describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executed") { - - val as = executeAnalyses( - Set( - EagerL1FieldAssignabilityAnalysis, - LazyInterProceduralEscapeAnalysis, - LazyReturnValueFreshnessAnalysis, - LazyFieldLocalityAnalysis, - EagerFieldAccessInformationAnalysis - ) - ) - as.propertyStore.shutdown() - validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) - } +// describe("the org.opalj.fpcf.analyses.L0FieldAssignability is executed") { +// +// val as = executeAnalyses( +// Set( +// EagerL0FieldAssignabilityAnalysis, +// LazyInterProceduralEscapeAnalysis, +// LazyReturnValueFreshnessAnalysis, +// LazyFieldLocalityAnalysis, +// EagerFieldAccessInformationAnalysis +// ) +// ) +// as.propertyStore.shutdown() +// validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) +// } + +// describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executed") { +// +// val as = executeAnalyses( +// Set( +// EagerL1FieldAssignabilityAnalysis, +// LazyInterProceduralEscapeAnalysis, +// LazyReturnValueFreshnessAnalysis, +// LazyFieldLocalityAnalysis, +// EagerFieldAccessInformationAnalysis +// ) +// ) +// as.propertyStore.shutdown() +// validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) +// } describe("the org.opalj.fpcf.analyses.L2FieldAssignability is executed") { diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala index 521bf3a690..4a3a74cbf8 100644 --- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala @@ -19,7 +19,6 @@ import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL0FieldAssignabilityAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL2FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL1PurityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL2PurityAnalysis @@ -72,7 +71,7 @@ class PurityTests extends PropertiesTest { val as = executeAnalyses( Set( EagerL1PurityAnalysis, - LazyL1FieldAssignabilityAnalysis, + // TODO find LazyL1FieldAssignabilityAnalysis, LazyFieldImmutabilityAnalysis, LazyClassImmutabilityAnalysis, LazyTypeImmutabilityAnalysis, diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala index 53f61713a3..3d7c5857a8 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala @@ -34,6 +34,8 @@ sealed trait FieldAssignability extends OrderedProperty with FieldAssignabilityP final def key: PropertyKey[FieldAssignability] = FieldAssignability.key def isImmutable = false + + def meet(that: FieldAssignability): FieldAssignability } object FieldAssignability extends FieldAssignabilityPropertyMetaInformation { @@ -73,9 +75,9 @@ case object EffectivelyNonAssignable extends NonAssignableField { def meet(other: FieldAssignability): FieldAssignability = if (other == NonAssignable) { - other - } else { this + } else { + other } } diff --git a/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala b/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala index 3c913338a3..253653e9c2 100644 --- a/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala +++ b/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala @@ -21,7 +21,6 @@ import org.opalj.fpcf.FPCFAnalysis import org.opalj.fpcf.PropertyStoreKey import org.opalj.tac.cg.RTACallGraphKey import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL1PurityAnalysis import org.opalj.util.Nanoseconds import org.opalj.util.PerformanceEvaluation.time @@ -45,7 +44,7 @@ class L1PuritySmokeTest extends AnyFunSpec with Matchers { val supportAnalyses: Set[ComputationSpecification[FPCFAnalysis]] = Set( EagerFieldAccessInformationAnalysis, - EagerL1FieldAssignabilityAnalysis, + // TODO find EagerL1FieldAssignabilityAnalysis, EagerFieldImmutabilityAnalysis, EagerClassImmutabilityAnalysis, EagerTypeImmutabilityAnalysis diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala b/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala index 7a07af9089..afe08f0d05 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala @@ -91,6 +91,7 @@ sealed abstract class Stmt[+V] extends ASTNode[V] { def isMonitorExit: Boolean = false def isThrow: Boolean = false def isArrayStore: Boolean = false + def isFieldWriteAccessStmt: Boolean = false def isPutStatic: Boolean = false def isPutField: Boolean = false def isMethodCall: Boolean = false @@ -656,6 +657,7 @@ sealed abstract class FieldWriteAccessStmt[+V] extends Stmt[V] { def declaredFieldType: FieldType def value: Expr[V] + override final def isFieldWriteAccessStmt: Boolean = true override final def asFieldWriteAccessStmt: this.type = this /** diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index 377be8d0b1..5464621a62 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -5,40 +5,28 @@ package fpcf package analyses package fieldassignability -import org.opalj.br.BooleanType -import org.opalj.br.ByteType -import org.opalj.br.CharType -import org.opalj.br.ClassType import org.opalj.br.DeclaredField import org.opalj.br.DefinedMethod -import org.opalj.br.DoubleType import org.opalj.br.Field -import org.opalj.br.FloatType -import org.opalj.br.IntegerType -import org.opalj.br.LongType import org.opalj.br.Method import org.opalj.br.PC -import org.opalj.br.ReferenceType -import org.opalj.br.ShortType import org.opalj.br.analyses.DeclaredFields import org.opalj.br.analyses.DeclaredFieldsKey import org.opalj.br.analyses.DeclaredMethods import org.opalj.br.analyses.DeclaredMethodsKey import org.opalj.br.analyses.ProjectInformationKeys +import org.opalj.br.analyses.SomeProject import org.opalj.br.analyses.cg.ClosedPackagesKey import org.opalj.br.analyses.cg.TypeExtensibilityKey +import org.opalj.br.fpcf.BasicFPCFEagerAnalysisScheduler +import org.opalj.br.fpcf.BasicFPCFLazyAnalysisScheduler import org.opalj.br.fpcf.ContextProviderKey import org.opalj.br.fpcf.FPCFAnalysis import org.opalj.br.fpcf.FPCFAnalysisScheduler import org.opalj.br.fpcf.analyses.ContextProvider -import org.opalj.br.fpcf.properties.AtMost import org.opalj.br.fpcf.properties.Context -import org.opalj.br.fpcf.properties.EscapeInCallee -import org.opalj.br.fpcf.properties.EscapeProperty -import org.opalj.br.fpcf.properties.EscapeViaReturn -import org.opalj.br.fpcf.properties.NoEscape -import org.opalj.br.fpcf.properties.cg.Callers import org.opalj.br.fpcf.properties.fieldaccess.AccessReceiver +import org.opalj.br.fpcf.properties.fieldaccess.FieldReadAccessInformation import org.opalj.br.fpcf.properties.fieldaccess.FieldWriteAccessInformation import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.EffectivelyNonAssignable @@ -46,53 +34,62 @@ import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.NonAssignable import org.opalj.fpcf.Entity import org.opalj.fpcf.EOptionP -import org.opalj.fpcf.FinalEP -import org.opalj.fpcf.FinalP import org.opalj.fpcf.InterimResult -import org.opalj.fpcf.InterimUBP import org.opalj.fpcf.ProperPropertyComputationResult import org.opalj.fpcf.PropertyBounds +import org.opalj.fpcf.PropertyStore import org.opalj.fpcf.Result import org.opalj.fpcf.SomeEOptionP import org.opalj.fpcf.SomeEPS -import org.opalj.fpcf.SomeInterimEP import org.opalj.fpcf.UBP -import org.opalj.tac.DUVar -import org.opalj.tac.Stmt -import org.opalj.tac.TACMethodParameter -import org.opalj.tac.TACode -import org.opalj.tac.common.DefinitionSite import org.opalj.tac.common.DefinitionSitesKey +import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites import org.opalj.tac.fpcf.properties.TACAI -import org.opalj.value.ValueInformation +/** + * TODO document + * + * @note This analysis is only ''soundy'' if the project does not contain native methods. + * + * @author Maximilian Rüsch + * @author Dominik Helm + */ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { trait AbstractFieldAssignabilityAnalysisState { val field: Field var fieldAssignability: FieldAssignability = NonAssignable - var fieldAccesses: Map[DefinedMethod, Set[(PC, AccessReceiver)]] = Map.empty - var escapeDependees: Set[EOptionP[(Context, DefinitionSite), EscapeProperty]] = Set.empty + var fieldWriteAccessDependee: Option[EOptionP[DeclaredField, FieldWriteAccessInformation]] = None + var initializerWrites: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + var nonInitializerWrites: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + + var fieldReadAccessDependee: Option[EOptionP[DeclaredField, FieldReadAccessInformation]] = None + var initializerReads: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + var nonInitializerReads: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + var tacDependees: Map[DefinedMethod, EOptionP[Method, TACAI]] = Map.empty - var callerDependees: Map[DefinedMethod, EOptionP[DefinedMethod, Callers]] = Map.empty.withDefault { dm => - propertyStore(dm, Callers.key) + + def forEachAccessInMethod(accesses: Map[Context, Set[(PC, AccessReceiver)]], definedMethod: DefinedMethod)( + f: (Context, Set[(PC, AccessReceiver)]) => Unit + ): Unit = { + accesses.iterator.filter(_._1.method eq definedMethod).foreach(kv => f(kv._1, kv._2)) } def hasDependees: Boolean = { - escapeDependees.nonEmpty || fieldWriteAccessDependee.exists(_.isRefinable) || - tacDependees.valuesIterator.exists(_.isRefinable) || callerDependees.valuesIterator.exists(_.isRefinable) + fieldWriteAccessDependee.exists(_.isRefinable) || + fieldReadAccessDependee.exists(_.isRefinable) || + tacDependees.valuesIterator.exists(_.isRefinable) } def dependees: Set[SomeEOptionP] = { - escapeDependees ++ fieldWriteAccessDependee.filter(_.isRefinable) ++ - callerDependees.valuesIterator.filter(_.isRefinable) ++ tacDependees.valuesIterator.filter(_.isRefinable) + tacDependees.valuesIterator.filter(_.isRefinable).toSet ++ + fieldWriteAccessDependee.filter(_.isRefinable) ++ + fieldReadAccessDependee.filter(_.isRefinable) } } - type V = DUVar[ValueInformation] - final val typeExtensibility = project.get(TypeExtensibilityKey) final val closedPackages = project.get(ClosedPackagesKey) final val definitionSites = project.get(DefinitionSitesKey) @@ -105,7 +102,7 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { case field: Field => determineFieldAssignability(field) case _ => - val m = entity.getClass.getSimpleName + " is not an org.opalj.br.Field" + val m = s"${entity.getClass.getSimpleName} is not a ${Field.getClass.getSimpleName}" throw new IllegalArgumentException(m) } } @@ -114,148 +111,124 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { def createState(field: Field): AnalysisState - /** - * Analyzes the field's assignability. - * - * This analysis is only ''soundy'' if the class file does not contain native methods and reflections. - * Fields can be manipulated by any given native method. - * Because the analysis cannot be aware of any given native method, - * they are not considered as well as reflections. - */ - private[analyses] def determineFieldAssignability( - field: Field - ): ProperPropertyComputationResult = { - - implicit val state: AnalysisState = createState(field) - - if (field.isFinal) - return Result(field, NonAssignable); - else - state.fieldAssignability = EffectivelyNonAssignable - - if (field.isPublic) - return Result(field, Assignable); - + private[analyses] def determineFieldAssignability(field: Field): ProperPropertyComputationResult = { val thisType = field.classFile.thisType - - if (field.isPublic) { - if (typeExtensibility(ClassType.Object).isYesOrUnknown) { - return Result(field, Assignable); - } - } else if (field.isProtected) { - if (typeExtensibility(thisType).isYesOrUnknown) { - return Result(field, Assignable); - } - if (!closedPackages(thisType.packageName)) { - return Result(field, Assignable); - } - } - if (field.isPackagePrivate) { - if (!closedPackages(thisType.packageName)) { + if (field.isNotFinal) { + if (field.isPublic) { return Result(field, Assignable); + } else if (field.isProtected) { + if (typeExtensibility(thisType).isYesOrUnknown || !closedPackages(thisType.packageName)) { + return Result(field, Assignable); + } + } else if (field.isPackagePrivate) { + if (!closedPackages(thisType.packageName)) { + return Result(field, Assignable); + } } } - val fwaiEP = propertyStore(declaredFields(field), FieldWriteAccessInformation.key) + val state: AnalysisState = createState(field) + state.fieldAssignability = if (field.isFinal) NonAssignable else EffectivelyNonAssignable - if (handleFieldWriteAccessInformation(fwaiEP)) - return Result(field, Assignable); - - createResult() + handleWriteAccessInformation(propertyStore(declaredFields(field), FieldWriteAccessInformation.key))(using state) } - /** - * Analyzes field writes for a single method, returning false if the field may still be - * effectively final and true otherwise. - */ - def methodUpdatesField( - method: DefinedMethod, - taCode: TACode[TACMethodParameter, V], - callers: Callers, - pc: PC, - receiver: AccessReceiver - )(implicit state: AnalysisState): Boolean - - /** - * Handles the influence of an escape property on the field immutability. - * - * @return true if the object - on which a field write occurred - escapes, false otherwise. - * @note (Re-)Adds dependees as necessary. - */ - def handleEscapeProperty( - ep: EOptionP[(Context, DefinitionSite), EscapeProperty] - )(implicit state: AnalysisState): Boolean = ep match { - case FinalP(NoEscape | EscapeInCallee | EscapeViaReturn) => - false - - case FinalP(AtMost(_)) => - true - - case FinalP(_) => - true // Escape state is worse than via return + def analyzeInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability + + def analyzeNonInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability + + def analyzeInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability + + def analyzeNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability - case InterimUBP(NoEscape | EscapeInCallee | EscapeViaReturn) => - state.escapeDependees += ep - false + def createResult()(implicit state: AnalysisState): ProperPropertyComputationResult = { + if (state.hasDependees && (state.fieldAssignability ne Assignable)) + InterimResult(state.field, lb = Assignable, ub = state.fieldAssignability, state.dependees, continuation) + else + Result(state.field, state.fieldAssignability) + } - case InterimUBP(AtMost(_)) => - true + def continuation(eps: SomeEPS)(implicit state: AnalysisState): ProperPropertyComputationResult = { + eps.pk match { + case FieldWriteAccessInformation.key => + handleWriteAccessInformation(eps.asInstanceOf[EOptionP[DeclaredField, FieldWriteAccessInformation]]) - case _: SomeInterimEP => - true // Escape state is worse than via return + case FieldReadAccessInformation.key => + handleReadAccessInformation(eps.asInstanceOf[EOptionP[DeclaredField, FieldReadAccessInformation]]) - case _ => - state.escapeDependees += ep - false - } - - /** - * Checks whether the object reference of a PutField does not escape (except for being returned). - */ - def referenceHasNotEscaped( - ref: V, - stmts: Array[Stmt[V]], - method: DefinedMethod, - callers: Callers - )(implicit state: AnalysisState): Boolean = { - ref.definedBy.forall { defSite => - if (defSite < 0) false // Must be locally created - else { - val definition = stmts(defSite).asAssignment - // Must either be null or freshly allocated - if (definition.expr.isNullExpr) true - else if (!definition.expr.isNew) false - else { - var hasEscaped = false - callers.forNewCalleeContexts(null, method) { context => - val entity = (context, definitionSites(method, definition.pc)) - val escapeProperty = propertyStore(entity, EscapeProperty.key) - hasEscaped ||= handleEscapeProperty(escapeProperty) + case TACAI.key => + val newEP = eps.asInstanceOf[EOptionP[Method, TACAI]] + val method = declaredMethods(newEP.e) + state.tacDependees += method -> newEP + val tac = newEP.ub.tac.get + // Renew field assignability analysis for all field accesses + state.forEachAccessInMethod(state.initializerWrites, method) { (context, accesses) => // TODO do for each access type + accesses.foreach { case (pc, receiver) => + val receiverVar = receiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (state.fieldAssignability != Assignable) + state.fieldAssignability = state.fieldAssignability.meet { + analyzeInitializerWrite(context, tac, pc, receiverVar) + } } - !hasEscaped } - } + createResult() } } - protected def handleFieldWriteAccessInformation( + private def handleWriteAccessInformation( newEP: EOptionP[DeclaredField, FieldWriteAccessInformation] - )(implicit state: AnalysisState): Boolean = { - val assignable = if (newEP.hasUBP) { - val newFai = newEP.ub - val (seenDirectAccesses, seenIndirectAccesses) = state.fieldWriteAccessDependee match { + )(implicit state: AnalysisState): ProperPropertyComputationResult = { + if (newEP.hasUBP) { + val (seenDirect, seenIndirect) = state.fieldWriteAccessDependee match { case Some(UBP(fai: FieldWriteAccessInformation)) => (fai.numDirectAccesses, fai.numIndirectAccesses) case _ => (0, 0) } + val (newDirect, newIndirect) = (newEP.ub.numDirectAccesses, newEP.ub.numIndirectAccesses) + if ((seenDirect + seenIndirect) == 0 && (newDirect + newIndirect) > 0) { + // We crossed from no writes to at least one write, so we potentially have to ensure a safe interaction + // with reads of the same field. Thus, they need to be analyzed. + handleReadAccessInformation(propertyStore(declaredFields(state.field), FieldReadAccessInformation.key)) + } state.fieldWriteAccessDependee = Some(newEP) - newFai.getNewestAccesses( - newFai.numDirectAccesses - seenDirectAccesses, - newFai.numIndirectAccesses - seenIndirectAccesses - ) exists { case (contextID, pc, receiver, _) => - val method = contextProvider.contextFromId(contextID).method.asDefinedMethod - state.fieldAccesses += method -> (state.fieldAccesses.getOrElse(method, Set.empty) + - ((pc, receiver))) + // Register all field accesses in the state first to enable cross access comparisons + newEP.ub.getNewestAccesses( + newDirect - seenDirect, + newIndirect - seenIndirect + ) foreach { case (contextID, pc, receiver, _) => + val context = contextProvider.contextFromId(contextID) + val method = context.method.asDefinedMethod + if (method.definedMethod.isInitializer) { + state.initializerWrites = state.initializerWrites.updatedWith(context) { + case None => Some(Set((pc, receiver))) + case Some(accesses) => Some(accesses.+((pc, receiver))) + } + } else { + state.nonInitializerWrites = state.nonInitializerWrites.updatedWith(context) { + case None => Some(Set((pc, receiver))) + case Some(accesses) => Some(accesses.+((pc, receiver))) + } + } val tacEP = state.tacDependees.get(method) match { case Some(tacEP) => tacEP @@ -264,112 +237,116 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { state.tacDependees += method -> tacEP tacEP } - - val callersEP = state.callerDependees.get(method) match { - case Some(callersEP) => callersEP - case None => - val callersEP = propertyStore(method, Callers.key) - state.callerDependees += method -> callersEP - callersEP + if (tacEP.hasUBP && state.fieldAssignability != Assignable) { + val tac = tacEP.ub.tac.get + val receiverVar = receiver.map(uVarForDefSites(_, tac.pcToIndex)) + state.fieldAssignability = state.fieldAssignability.meet { + if (method.definedMethod.isInitializer) + analyzeInitializerWrite(context, tac, pc, receiverVar) + else + analyzeNonInitializerWrite(context, tac, pc, receiverVar) + } } - - if (tacEP.hasUBP && callersEP.hasUBP) - methodUpdatesField(method, tacEP.ub.tac.get, callersEP.ub, pc, receiver) - else - false } } else { state.fieldWriteAccessDependee = Some(newEP) - false } - assignable + createResult() } - /** - * Continuation function handling updates to the FieldPrematurelyRead property or to the purity - * property of the method that initializes a (potentially) lazy initialized field. - */ - def c(eps: SomeEPS)(implicit state: AnalysisState): ProperPropertyComputationResult = { - val isNonFinal = eps.pk match { - case EscapeProperty.key => - val newEP = eps.asInstanceOf[EOptionP[(Context, DefinitionSite), EscapeProperty]] - state.escapeDependees = state.escapeDependees.filter(_.e != newEP.e) - handleEscapeProperty(newEP) - case TACAI.key => - val newEP = eps.asInstanceOf[EOptionP[Method, TACAI]] - val method = declaredMethods(newEP.e) - val accesses = state.fieldAccesses.get(method) - state.tacDependees += method -> newEP - val callersProperty = state.callerDependees(method) - if (callersProperty.hasUBP && accesses.isDefined) - accesses.get.exists(access => - methodUpdatesField(method, newEP.ub.tac.get, callersProperty.ub, access._1, access._2) - ) - else false - case Callers.key => - val newEP = eps.asInstanceOf[EOptionP[DefinedMethod, Callers]] - val method = newEP.e - val accesses = state.fieldAccesses.get(method) - state.callerDependees += newEP.e -> newEP - val tacProperty = state.tacDependees(method) - if (tacProperty.hasUBP && tacProperty.ub.tac.isDefined && accesses.isDefined) - accesses.get.exists(access => - methodUpdatesField(method, tacProperty.ub.tac.get, newEP.ub, access._1, access._2) - ) - else false - case FieldWriteAccessInformation.key => - val newEP = eps.asInstanceOf[EOptionP[DeclaredField, FieldWriteAccessInformation]] - handleFieldWriteAccessInformation(newEP) + private def handleReadAccessInformation( + newEP: EOptionP[DeclaredField, FieldReadAccessInformation] + )(implicit state: AnalysisState): ProperPropertyComputationResult = { + if (newEP.hasUBP) { + val (seenDirect, seenIndirect) = state.fieldReadAccessDependee match { + case Some(UBP(fai: FieldReadAccessInformation)) => (fai.numDirectAccesses, fai.numIndirectAccesses) + case _ => (0, 0) + } + val (newDirect, newIndirect) = (newEP.ub.numDirectAccesses, newEP.ub.numIndirectAccesses) + state.fieldReadAccessDependee = Some(newEP) + + // Register all field accesses in the state first to enable cross access comparisons + newEP.ub.getNewestAccesses( + newDirect - seenDirect, + newIndirect - seenIndirect + ) foreach { case (contextID, pc, receiver, _) => + val context = contextProvider.contextFromId(contextID) + val method = context.method.asDefinedMethod + if (method.definedMethod.isInitializer) { + state.initializerReads = state.initializerReads.updatedWith(context) { + case None => Some(Set((pc, receiver))) + case Some(accesses) => Some(accesses.+((pc, receiver))) + } + } else { + state.nonInitializerReads = state.nonInitializerReads.updatedWith(context) { + case None => Some(Set((pc, receiver))) + case Some(accesses) => Some(accesses.+((pc, receiver))) + } + } + + val tacEP = state.tacDependees.get(method) match { + case Some(tacEP) => tacEP + case None => + val tacEP = propertyStore(method.definedMethod, TACAI.key) + state.tacDependees += method -> tacEP + tacEP + } + if (tacEP.hasUBP && state.fieldAssignability != Assignable) { + val tac = tacEP.ub.tac.get + val receiverVar = receiver.map(uVarForDefSites(_, tac.pcToIndex)) + state.fieldAssignability = state.fieldAssignability.meet { + if (method.definedMethod.isInitializer) + analyzeInitializerRead(context, tac, pc, receiverVar) + else + analyzeNonInitializerRead(context, tac, pc, receiverVar) + } + } + } + } else { + state.fieldReadAccessDependee = Some(newEP) } - if (isNonFinal) - Result(state.field, Assignable) - else - createResult() + createResult() } - def createResult()(implicit state: AnalysisState): ProperPropertyComputationResult = { - if (state.hasDependees && (state.fieldAssignability ne Assignable)) { - InterimResult( - state.field, - Assignable, - state.fieldAssignability, - state.dependees, - c - ) - } else { - Result(state.field, state.fieldAssignability) - } + /** + * Determines whether the basic block of a given index dominates the basic block of the other index or is executed + * before the other index in the case of both indexes belonging to the same basic block. + */ + protected def dominates( + potentiallyDominatorIndex: Int, + potentiallyDominatedIndex: Int, + taCode: TACode[TACMethodParameter, V] + ): Boolean = { + val bbPotentiallyDominator = taCode.cfg.bb(potentiallyDominatorIndex) + val bbPotentiallyDominated = taCode.cfg.bb(potentiallyDominatedIndex) + taCode.cfg.dominatorTree + .strictlyDominates(bbPotentiallyDominator.nodeId, bbPotentiallyDominated.nodeId) || + bbPotentiallyDominator == bbPotentiallyDominated && potentiallyDominatorIndex < potentiallyDominatedIndex } /** - * Returns the initialization value of a given type. + * Provided with two PCs, determines whether there exists a path from the first PC to the second PC in the context + * of the provided TAC and attached CFG. This check is not reflexive. + * + * IMPROVE abort on path found instead of computing all reachable BBs */ - def getDefaultValues()(implicit state: AnalysisState): Set[Any] = state.field.fieldType match { - case FloatType | ClassType.Float => Set(0.0f) - case DoubleType | ClassType.Double => Set(0.0d) - case LongType | ClassType.Long => Set(0L) - case CharType | ClassType.Character => Set('\u0000') - case BooleanType | ClassType.Boolean => Set(false) - case IntegerType | - ClassType.Integer | - ByteType | - ClassType.Byte | - ShortType | - ClassType.Short => Set(0) - case ClassType.String => Set("", null) - case _: ReferenceType => Set(null) + protected def pathExists(fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]): Boolean = { + val firstBB = tac.cfg.bb(tac.pcToIndex(fromPC)) + val secondBB = tac.cfg.bb(tac.pcToIndex(toPC)) + + if (firstBB == secondBB) fromPC < toPC + else firstBB.reachable().contains(secondBB) } } -trait AbstractFieldAssignabilityAnalysisScheduler extends FPCFAnalysisScheduler { +sealed trait AbstractFieldAssignabilityAnalysisScheduler extends FPCFAnalysisScheduler { override def uses: Set[PropertyBounds] = PropertyBounds.ubs( TACAI, - EscapeProperty, FieldWriteAccessInformation, - Callers + FieldReadAccessInformation ) override def requiredProjectInformation: ProjectInformationKeys = Seq( @@ -383,3 +360,41 @@ trait AbstractFieldAssignabilityAnalysisScheduler extends FPCFAnalysisScheduler final def derivedProperty: PropertyBounds = PropertyBounds.ub(FieldAssignability) } + +/** + * Executor for the eager field assignability analysis. + */ +trait AbstractEagerFieldAssignabilityAnalysisScheduler + extends AbstractFieldAssignabilityAnalysisScheduler + with BasicFPCFEagerAnalysisScheduler { + + override def derivesEagerly: Set[PropertyBounds] = Set(derivedProperty) + + override def derivesCollaboratively: Set[PropertyBounds] = Set.empty + + override def start(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { + val analysis = newAnalysis(p) + ps.scheduleEagerComputationsForEntities(p.allFields)(analysis.determineFieldAssignability) + analysis + } + + def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis +} + +/** + * Executor for the lazy field assignability analysis. + */ +trait AbstractLazyFieldAssignabilityAnalysisScheduler + extends AbstractFieldAssignabilityAnalysisScheduler + with BasicFPCFLazyAnalysisScheduler { + + override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) + + override def register(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { + val analysis = newAnalysis(p) + ps.registerLazyPropertyComputation(FieldAssignability.key, analysis.doDetermineFieldAssignability) + analysis + } + + def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index 0912bac99a..dd7c9c01ab 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -5,213 +5,63 @@ package fpcf package analyses package fieldassignability -import org.opalj.br.DeclaredField import org.opalj.br.Field -import org.opalj.br.analyses.DeclaredFields -import org.opalj.br.analyses.DeclaredFieldsKey -import org.opalj.br.analyses.DeclaredMethods -import org.opalj.br.analyses.DeclaredMethodsKey -import org.opalj.br.analyses.ProjectInformationKeys +import org.opalj.br.PC import org.opalj.br.analyses.SomeProject -import org.opalj.br.fpcf.BasicFPCFEagerAnalysisScheduler -import org.opalj.br.fpcf.BasicFPCFLazyAnalysisScheduler -import org.opalj.br.fpcf.ContextProviderKey -import org.opalj.br.fpcf.FPCFAnalysis -import org.opalj.br.fpcf.FPCFAnalysisScheduler -import org.opalj.br.fpcf.analyses.ContextProvider -import org.opalj.br.fpcf.properties.fieldaccess.FieldWriteAccessInformation +import org.opalj.br.fpcf.properties.Context import org.opalj.br.fpcf.properties.immutability.Assignable -import org.opalj.br.fpcf.properties.immutability.EffectivelyNonAssignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.NonAssignable -import org.opalj.fpcf.Entity -import org.opalj.fpcf.EOptionP -import org.opalj.fpcf.InterimResult -import org.opalj.fpcf.ProperPropertyComputationResult -import org.opalj.fpcf.PropertyBounds -import org.opalj.fpcf.PropertyStore -import org.opalj.fpcf.Result -import org.opalj.fpcf.SomeEOptionP -import org.opalj.fpcf.SomeEPS -import org.opalj.fpcf.UBP +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode /** - * Determines if a private, static, non-final field is always initialized at most once or - * if a field is or can be mutated after (lazy) initialization. Field read and writes at - * initialization time (e.g., if the current class object is registered in some publicly - * available data-store) are not considered. This is in-line with the semantics of final, - * which also does not prevent reads of partially initialized objects. + * A field assignability analysis that treats every field access (reads and writes) as unsafe and thus immediately + * marks the field as assignable if one such read / write is detected. + * + * @author Maximilian Rüsch + * @author Dominik Helm */ -class L0FieldAssignabilityAnalysis private[analyses] (val project: SomeProject) extends FPCFAnalysis { - - final val declaredFields: DeclaredFields = project.get(DeclaredFieldsKey) - final val contextProvider: ContextProvider = project.get(ContextProviderKey) - implicit val declaredMethods: DeclaredMethods = project.get(DeclaredMethodsKey) - - case class L0FieldAssignabilityAnalysisState(field: Field) { - var fieldAssignability: FieldAssignability = EffectivelyNonAssignable // Assume this as the optimistic default - var latestFieldWriteAccessInformation: Option[EOptionP[DeclaredField, FieldWriteAccessInformation]] = None - def hasDependees: Boolean = latestFieldWriteAccessInformation.exists(_.isRefinable) - def dependees: Set[SomeEOptionP] = latestFieldWriteAccessInformation.filter(_.isRefinable).toSet - } - type AnalysisState = L0FieldAssignabilityAnalysisState - - /** - * Invoked for in the lazy computation case. - * Final fields are considered [[org.opalj.br.fpcf.properties.immutability.NonAssignable]], non-final and - * non-private fields or fields of library classes whose method bodies are not available are - * considered [[org.opalj.br.fpcf.properties.immutability.Assignable]]. - * For all other cases the call is delegated to [[determineFieldAssignability]]. - */ - def determineFieldAssignabilityLazy(e: Entity): ProperPropertyComputationResult = { - e match { - case field: Field => determineFieldAssignability(field) - case _ => throw new IllegalArgumentException(s"$e is not a Field") - } - } - - /** - * Analyzes the mutability of private static non-final fields. - * - * This analysis is only ''defined and soundy'' if the class file does not contain native - * methods and the method body of all non-abstract methods is available. - * (If the analysis is scheduled using its companion object, all class files with - * native methods are filtered.) - * - * @param field A field without native methods and where the method body of all - * non-abstract methods is available. - */ - def determineFieldAssignability(field: Field): ProperPropertyComputationResult = { - implicit val state: L0FieldAssignabilityAnalysisState = L0FieldAssignabilityAnalysisState(field) - - if (field.isFinal) - return Result(field, NonAssignable); - - if (!field.isPrivate) - return Result(field, Assignable); - - if (!field.isStatic) - return Result(field, Assignable); - - if (field.classFile.methods.exists(_.isNative)) - return Result(field, Assignable); - - val faiEP = propertyStore(declaredFields(field), FieldWriteAccessInformation.key) - if (handleFieldWriteAccessInformation(faiEP)) - return Result(field, Assignable) - - createResult() - } - - /** - * Processes the given field access information to evaluate if the given field is written statically in the method at - * the given PCs. Updates the state to account for the new value. - */ - def handleFieldWriteAccessInformation( - faiEP: EOptionP[DeclaredField, FieldWriteAccessInformation] - )(implicit state: AnalysisState): Boolean = { - val assignable = if (faiEP.hasUBP) { - val (seenDirectAccesses, seenIndirectAccesses) = state.latestFieldWriteAccessInformation match { - case Some(UBP(fai: FieldWriteAccessInformation)) => (fai.numDirectAccesses, fai.numIndirectAccesses) - case _ => (0, 0) - } - - faiEP.ub.getNewestAccesses( - faiEP.ub.numDirectAccesses - seenDirectAccesses, - faiEP.ub.numIndirectAccesses - seenIndirectAccesses - ) exists { case (contextID, _, receiver, _) => - val method = contextProvider.contextFromId(contextID).method.definedMethod - if (method.isStaticInitializer) { - if (receiver.isDefined) { - // If a receiver is defined, we know that the access was not static - // IMPROVE: Add static information to accesses and resolve this - false - } else { - // As a fallback, we soundly assume assignability - true - } - } else - false - } - } else - false - - state.latestFieldWriteAccessInformation = Some(faiEP) - assignable - } - - def createResult()(implicit state: AnalysisState): ProperPropertyComputationResult = { - if (state.hasDependees && (state.fieldAssignability ne Assignable)) { - InterimResult( - state.field, - Assignable, - state.fieldAssignability, - state.dependees, - continuation - ) - } else { - Result(state.field, state.fieldAssignability) - } - } - - def continuation(eps: SomeEPS)(implicit state: AnalysisState): ProperPropertyComputationResult = { - if (handleFieldWriteAccessInformation(eps.asInstanceOf[EOptionP[DeclaredField, FieldWriteAccessInformation]])) - Result(state.field, Assignable) - else - createResult() - } +class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) + extends AbstractFieldAssignabilityAnalysis { + + case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState + type AnalysisState = State + override def createState(field: Field): AnalysisState = State(field) + + override def analyzeInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = Assignable + + override def analyzeNonInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = NonAssignable + + override def analyzeInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = NonAssignable // TODO handle constructor escapes + + override def analyzeNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = Assignable } -trait L0FieldAssignabilityAnalysisScheduler extends FPCFAnalysisScheduler { - - override def requiredProjectInformation: ProjectInformationKeys = Seq(DeclaredMethodsKey, DeclaredFieldsKey) - - override final def uses: Set[PropertyBounds] = PropertyBounds.ubs(FieldWriteAccessInformation) - - final def derivedProperty: PropertyBounds = { - // currently, the analysis will derive the final result in a single step - PropertyBounds.finalP(FieldAssignability) - } +object EagerL0FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L0FieldAssignabilityAnalysis(p) } -/** - * Factory object to create instances of the FieldImmutabilityAnalysis. - */ -object EagerL0FieldAssignabilityAnalysis - extends L0FieldAssignabilityAnalysisScheduler - with BasicFPCFEagerAnalysisScheduler { - - override def derivesEagerly: Set[PropertyBounds] = Set(derivedProperty) - - override def derivesCollaboratively: Set[PropertyBounds] = Set.empty - - override def start(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { - val analysis = new L0FieldAssignabilityAnalysis(p) - val classFileCandidates = - if (p.libraryClassFilesAreInterfacesOnly) - p.allProjectClassFiles - else - p.allClassFiles - val fields = { - classFileCandidates.filter(cf => cf.methods.forall(m => !m.isNative)).flatMap(_.fields) - } - ps.scheduleEagerComputationsForEntities(fields)(analysis.determineFieldAssignability) - analysis - } -} - -object LazyL0FieldAssignabilityAnalysis - extends L0FieldAssignabilityAnalysisScheduler - with BasicFPCFLazyAnalysisScheduler { - - override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) - - override def register(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { - val analysis = new L0FieldAssignabilityAnalysis(p) - ps.registerLazyPropertyComputation( - FieldAssignability.key, - (field: Field) => analysis.determineFieldAssignabilityLazy(field) - ) - analysis - } +object LazyL0FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L0FieldAssignabilityAnalysis(p) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala deleted file mode 100644 index 1d0ed2eb20..0000000000 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ /dev/null @@ -1,110 +0,0 @@ -/* BSD 2-Clause License - see OPAL/LICENSE for details. */ -package org.opalj -package tac -package fpcf -package analyses -package fieldassignability - -import org.opalj.br.DefinedMethod -import org.opalj.br.Field -import org.opalj.br.PC -import org.opalj.br.analyses.SomeProject -import org.opalj.br.fpcf.BasicFPCFEagerAnalysisScheduler -import org.opalj.br.fpcf.BasicFPCFLazyAnalysisScheduler -import org.opalj.br.fpcf.FPCFAnalysis -import org.opalj.br.fpcf.properties.cg.Callers -import org.opalj.br.fpcf.properties.fieldaccess.AccessReceiver -import org.opalj.br.fpcf.properties.immutability.FieldAssignability -import org.opalj.fpcf.PropertyBounds -import org.opalj.fpcf.PropertyStore -import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites - -/** - * Simple analysis that checks if a private (static or instance) field is always initialized at - * most once or if a field is or can be mutated after (lazy) initialization. - * - * @note Requires that the 3-address code's expressions are not deeply nested. - * @author Tobias Roth - * @author Dominik Helm - * @author Florian Kübler - * @author Michael Eichberg - */ -class L1FieldAssignabilityAnalysis private[analyses] (val project: SomeProject) - extends AbstractFieldAssignabilityAnalysis { - - case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState - type AnalysisState = State - override def createState(field: Field): AnalysisState = State(field) - - /** - * Analyzes field writes for a single method, returning false if the field may still be - * effectively final and true otherwise. - */ - def methodUpdatesField( - definedMethod: DefinedMethod, - taCode: TACode[TACMethodParameter, V], - callers: Callers, - pc: PC, - receiver: AccessReceiver - )(implicit state: AnalysisState): Boolean = { - val stmts = taCode.stmts - val method = definedMethod.definedMethod - - if (receiver.isDefined) { - val objRef = uVarForDefSites(receiver.get, taCode.pcToIndex).asVar - // note that here we assume real three address code (flat hierarchy) - - // for instance fields it is okay if they are written in the - // constructor (w.r.t. the currently initialized object!) - - // If the field that is written is not the one referred to by the - // self reference, it is not effectively final. - - // However, a method (e.g. clone) may instantiate a new object and - // write the field as long as that new object did not yet escape. - (!method.isConstructor || - objRef.definedBy != SelfReferenceParameter) && - !referenceHasNotEscaped(objRef, stmts, definedMethod, callers) - } else { - !method.isStaticInitializer - } - } -} - -/** - * Executor for the eager field assignability analysis. - */ -object EagerL1FieldAssignabilityAnalysis - extends AbstractFieldAssignabilityAnalysisScheduler - with BasicFPCFEagerAnalysisScheduler { - - override def derivesEagerly: Set[PropertyBounds] = Set(derivedProperty) - - override def derivesCollaboratively: Set[PropertyBounds] = Set.empty - - override def start(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { - val analysis = new L1FieldAssignabilityAnalysis(p) - val fields = p.allFields - ps.scheduleEagerComputationsForEntities(fields)(analysis.determineFieldAssignability) - analysis - } -} - -/** - * Executor for the lazy field assignability analysis. - */ -object LazyL1FieldAssignabilityAnalysis - extends AbstractFieldAssignabilityAnalysisScheduler - with BasicFPCFLazyAnalysisScheduler { - - override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) - - override def register(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { - val analysis = new L1FieldAssignabilityAnalysis(p) - ps.registerLazyPropertyComputation( - FieldAssignability.key, - analysis.doDetermineFieldAssignability - ) - analysis - } -} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 5aebf66fb3..ed06e17198 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -5,64 +5,17 @@ package fpcf package analyses package fieldassignability -import scala.annotation.switch - -import scala.collection.mutable -import scala.util.boundary -import scala.util.boundary.break - -import org.opalj.RelationalOperators.EQ -import org.opalj.RelationalOperators.NE -import org.opalj.ai.isFormalParameter -import org.opalj.br.ClassType -import org.opalj.br.DeclaredField -import org.opalj.br.DefinedMethod import org.opalj.br.Field -import org.opalj.br.FieldType -import org.opalj.br.Method import org.opalj.br.PC -import org.opalj.br.PCs import org.opalj.br.analyses.SomeProject -import org.opalj.br.cfg.BasicBlock -import org.opalj.br.cfg.CFGNode -import org.opalj.br.fpcf.BasicFPCFEagerAnalysisScheduler -import org.opalj.br.fpcf.BasicFPCFLazyAnalysisScheduler import org.opalj.br.fpcf.FPCFAnalysis -import org.opalj.br.fpcf.properties.cg.Callers -import org.opalj.br.fpcf.properties.fieldaccess.AccessParameter -import org.opalj.br.fpcf.properties.fieldaccess.AccessReceiver -import org.opalj.br.fpcf.properties.fieldaccess.FieldReadAccessInformation -import org.opalj.br.fpcf.properties.fieldaccess.FieldWriteAccessInformation +import org.opalj.br.fpcf.properties.Context import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability -import org.opalj.br.fpcf.properties.immutability.LazilyInitialized -import org.opalj.br.fpcf.properties.immutability.UnsafelyLazilyInitialized -import org.opalj.collection.immutable.IntTrieSet -import org.opalj.fpcf.EOptionP -import org.opalj.fpcf.ProperPropertyComputationResult -import org.opalj.fpcf.PropertyBounds -import org.opalj.fpcf.PropertyStore -import org.opalj.fpcf.Result -import org.opalj.fpcf.SomeEOptionP -import org.opalj.fpcf.SomeEPS -import org.opalj.fpcf.UBP -import org.opalj.tac.CaughtException -import org.opalj.tac.ClassConst -import org.opalj.tac.Compare -import org.opalj.tac.Expr -import org.opalj.tac.FieldWriteAccessStmt -import org.opalj.tac.GetField -import org.opalj.tac.GetStatic -import org.opalj.tac.If -import org.opalj.tac.MonitorEnter -import org.opalj.tac.MonitorExit -import org.opalj.tac.PrimitiveTypecastExpr +import org.opalj.br.fpcf.properties.immutability.NonAssignable import org.opalj.tac.SelfReferenceParameter -import org.opalj.tac.Stmt import org.opalj.tac.TACMethodParameter import org.opalj.tac.TACode -import org.opalj.tac.Throw -import org.opalj.tac.VirtualFunctionCall import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** @@ -73,967 +26,216 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites * @author Dominik Helm * @author Florian Kübler * @author Michael Eichberg + * @author Maximilian Rüsch */ -class L2FieldAssignabilityAnalysis private[analyses] (val project: SomeProject) +class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis + with LazyInitializationAnalysis with FPCFAnalysis { - val considerLazyInitialization: Boolean = - project.config.getBoolean( - "org.opalj.fpcf.analyses.L2FieldAssignabilityAnalysis.considerLazyInitialization" - ) - - /** - * Analyzes field writes for a single method, returning false if the field may still be - * effectively final and true otherwise. - */ - def methodUpdatesField( - definedMethod: DefinedMethod, - taCode: TACode[TACMethodParameter, V], - callers: Callers, - pc: PC, - receiver: AccessReceiver - )(implicit state: AnalysisState): Boolean = { - val field = state.field - val method = definedMethod.definedMethod - val stmts = taCode.stmts - val receiverVar = receiver.map(uVarForDefSites(_, taCode.pcToIndex)) - - val index = taCode.pcToIndex(pc) - if (method.isInitializer) { - if (field.isStatic) { - method.isConstructor - } else { - receiverVar.isDefined && receiverVar.get.definedBy != SelfReferenceParameter - } - } else { - if (field.isStatic || receiverVar.isDefined && receiverVar.get.definedBy == SelfReferenceParameter) { - // A field written outside an initializer must be lazily initialized or it is assignable - if (considerLazyInitialization) { - isAssignable(index, getDefaultValues(), method, taCode) - } else - true - } else if (receiverVar.isDefined && !referenceHasNotEscaped(receiverVar.get, stmts, definedMethod, callers)) { - // Here the clone pattern is determined among others - // - // note that here we assume real three address code (flat hierarchy) - - // for instance fields it is okay if they are written in the - // constructor (w.r.t. the currently initialized object!) - - // If the field that is written is not the one referred to by the - // self reference, it is not effectively final. - - // However, a method (e.g. clone) may instantiate a new object and - // write the field as long as that new object did not yet escape. - true - } else { - checkWriteDominance(definedMethod, taCode, receiverVar, index) - } - - } - } - - private def checkWriteDominance( - definedMethod: DefinedMethod, - taCode: TACode[TACMethodParameter, V], - receiverVar: Option[V], - index: Int - )(implicit state: State): Boolean = { - val stmts = taCode.stmts - - val writes = state.fieldWriteAccessDependee.get.ub.accesses - val writesInMethod = writes.filter { w => contextProvider.contextFromId(w._1).method eq definedMethod }.toSeq - - if (writesInMethod.distinctBy(_._2).size > 1) - return true; // Field is written in multiple locations, thus must be assignable - - // If we have no information about the receiver, we soundly return - if (receiverVar.isEmpty) - return true; - - val assignedValueObject = receiverVar.get - if (assignedValueObject.definedBy.exists(_ < 0)) - return true; - - val assignedValueObjectVar = stmts(assignedValueObject.definedBy.head).asAssignment.targetVar.asVar - - val fieldWriteInMethodIndex = taCode.pcToIndex(writesInMethod.head._2) - if (assignedValueObjectVar != null && !assignedValueObjectVar.usedBy.forall { index => - val stmt = stmts(index) - - fieldWriteInMethodIndex == index || // The value is itself written to another object - // IMPROVE: Can we use field access information to care about reflective accesses here? - stmt.isPutField && stmt.asPutField.name != state.field.name || - stmt.isAssignment && stmt.asAssignment.targetVar == assignedValueObjectVar || - stmt.isMethodCall && stmt.asMethodCall.name == "" || - // CHECK do we really need the taCode here? - dominates(fieldWriteInMethodIndex, index, taCode) - } - ) - return true; - - val writeAccess = (definedMethod, taCode, receiverVar, index) - - if (state.fieldReadAccessDependee.isEmpty) { - state.fieldReadAccessDependee = - Some(propertyStore(declaredFields(state.field), FieldReadAccessInformation.key)) - } - - val fraiEP = state.fieldReadAccessDependee.get - - if (fraiEP.hasUBP && fieldReadsNotDominated(fraiEP.ub, 0, 0, Seq(writeAccess))) - return true; - - state.openWrites ::= writeAccess - - false - } - - override def c(eps: SomeEPS)(implicit state: State): ProperPropertyComputationResult = { - eps.pk match { - case FieldReadAccessInformation.key => - val newEP = eps.asInstanceOf[EOptionP[DeclaredField, FieldReadAccessInformation]] - val reads = newEP.ub - val (seenDirectAccesses, seenIndirectAccesses) = state.fieldReadAccessDependee match { - case Some(UBP(fai: FieldReadAccessInformation)) => (fai.numDirectAccesses, fai.numIndirectAccesses) - case _ => (0, 0) - } - - if (fieldReadsNotDominated(reads, seenDirectAccesses, seenIndirectAccesses, state.openWrites)) - return Result(state.field, Assignable); - - if (state.checkLazyInit.isDefined) { - val (method, guardIndex, writeIndex, taCode) = state.checkLazyInit.get - if (doFieldReadsEscape( - reads.getNewestAccesses( - reads.numDirectAccesses - seenDirectAccesses, - reads.numIndirectAccesses - seenIndirectAccesses - ).toSeq, - method, - guardIndex, - writeIndex, - taCode - ) - ) - return Result(state.field, Assignable); - } - - state.fieldReadAccessDependee = Some(newEP) - createResult() - - case _ => - super.c(eps) - } - } - - override protected def handleFieldWriteAccessInformation( - newEP: EOptionP[DeclaredField, FieldWriteAccessInformation] - )(implicit state: State): Boolean = { - val openWrites = state.openWrites - state.openWrites = List.empty - - state.checkLazyInit.isDefined && hasMultipleNonConstructorWrites(state.checkLazyInit.get._1) || - super.handleFieldWriteAccessInformation(newEP) || - openWrites.exists { case (method, tac, receiver, index) => - checkWriteDominance(method, tac, receiver, index) - } - } - - private def fieldReadsNotDominated( - fieldReadAccessInformation: FieldReadAccessInformation, - seenDirectAccesses: Int, - seenIndirectAccesses: Int, - writes: Seq[(DefinedMethod, TACode[TACMethodParameter, V], Option[V], Int)] - )(implicit state: State): Boolean = { - writes.exists { case (writeMethod, _, _, writeIndex) => - fieldReadAccessInformation.getNewestAccesses( - fieldReadAccessInformation.numDirectAccesses - seenDirectAccesses, - fieldReadAccessInformation.numIndirectAccesses - seenIndirectAccesses - ).exists { case (readContextID, readPC, readReceiver, _) => - val method = contextProvider.contextFromId(readContextID).method - (writeMethod eq method) && { - val taCode = state.tacDependees(method.asDefinedMethod).ub.tac.get - - if (readReceiver.isDefined && readReceiver.get._2.forall(isFormalParameter)) { - false - } else { - !dominates(writeIndex, taCode.pcToIndex(readPC), taCode) - } - } - } - } - } - - case class State( - field: Field - ) extends AbstractFieldAssignabilityAnalysisState { - var checkLazyInit: Option[(Method, Int, Int, TACode[TACMethodParameter, V])] = None - var openWrites = List.empty[(DefinedMethod, TACode[TACMethodParameter, V], Option[V], PC)] - - var fieldReadAccessDependee: Option[EOptionP[DeclaredField, FieldReadAccessInformation]] = None - - override def hasDependees: Boolean = fieldReadAccessDependee.exists(_.isRefinable) || super.hasDependees - - override def dependees: Set[SomeEOptionP] = super.dependees ++ fieldReadAccessDependee.filter(_.isRefinable) - } - + case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState + with LazyInitializationAnalysisState type AnalysisState = State - override def createState(field: Field): AnalysisState = State(field) - /** - * Determines whether the basic block of a given index dominates the basic block of the other index or is executed - * before the other index in the case of both indexes belonging to the same basic block. - */ - def dominates( - potentiallyDominatorIndex: Int, - potentiallyDominatedIndex: Int, - taCode: TACode[TACMethodParameter, V] - ): Boolean = { - val bbPotentiallyDominator = taCode.cfg.bb(potentiallyDominatorIndex) - val bbPotentiallyDominated = taCode.cfg.bb(potentiallyDominatedIndex) - taCode.cfg.dominatorTree - .strictlyDominates(bbPotentiallyDominator.nodeId, bbPotentiallyDominated.nodeId) || - bbPotentiallyDominator == bbPotentiallyDominated && potentiallyDominatorIndex < potentiallyDominatedIndex - } - - // lazy initialization: - - /** - * Handles the lazy initialization determination for a field write in a given method - * @author Tobias Roth - * @return true if no lazy initialization was recognized - */ - def isAssignable( - writeIndex: Int, - defaultValues: Set[Any], - method: Method, - taCode: TACode[TACMethodParameter, V] - )(implicit state: AnalysisState): Boolean = { - state.fieldAssignability = determineLazyInitialization(writeIndex, defaultValues, method, taCode) - state.fieldAssignability eq Assignable - } - - def hasMultipleNonConstructorWrites(method: Method)(implicit state: AnalysisState): Boolean = { - val writes = state.fieldWriteAccessDependee.get.ub.accesses.toSeq - - // prevents writes outside the method and the constructor - writes.exists(w => { - val accessingMethod = contextProvider.contextFromId(w._1).method.definedMethod - (accessingMethod ne method) && !accessingMethod.isInitializer - }) || - writes.iterator.distinctBy(_._1).size < writes.size // More than one write per method was detected - } - - /** - * Determines the kind of lazy initialization of a given field in the given method through a given field write. - * @author Tobias Roth - */ - def determineLazyInitialization( - writeIndex: Int, - defaultValues: Set[Any], - method: Method, - taCode: TACode[TACMethodParameter, V] - )(implicit state: AnalysisState): FieldAssignability = { - if (hasMultipleNonConstructorWrites(method)) - return Assignable; - - val code = taCode.stmts - val cfg = taCode.cfg - val write = code(writeIndex).asFieldWriteAccessStmt - val writeBB = cfg.bb(writeIndex).asBasicBlock - val resultCatchesAndThrows = findCatchesAndThrows(taCode) - - /** - * Determines whether all caught exceptions are thrown afterwards - */ - def noInterferingExceptions(): Boolean = - resultCatchesAndThrows._1.forall { - case (catchPC, originsCaughtException) => - resultCatchesAndThrows._2.exists { - case (throwPC, throwDefinitionSites) => - dominates(taCode.pcToIndex(catchPC), taCode.pcToIndex(throwPC), taCode) && - throwDefinitionSites == originsCaughtException // throwing and catching same exceptions - } - } - - val findGuardsResult = findGuards(writeIndex, defaultValues, taCode) - - // no guard -> no Lazy Initialization - if (findGuardsResult.isEmpty) + override def analyzeInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: State): FieldAssignability = { + val assignability = completeLazyInitPatternForInitializerRead() + if (assignability == Assignable) return Assignable; - // guardIndex: for debugging purpose - val (readIndex, guardIndex, defaultCaseIndex, elseCaseIndex) = findGuardsResult.head - - // The field has to be written when the guard is in the default-case branch - if (!dominates(defaultCaseIndex, writeIndex, taCode)) + // Initializer reads are incompatible with arbitrary writes in other methods + if (state.nonInitializerWrites.nonEmpty) return Assignable; - val elseBB = cfg.bb(elseCaseIndex) - - // prevents wrong control flow - if (isTransitivePredecessor(elseBB, writeBB)) + // Analyzing read-write paths interprocedurally is not supported yet + if (state.initializerWrites.exists(_._1.method ne context.method)) return Assignable; - if (method.returnType == state.field.fieldType) { - // prevents that another value than the field value is returned with the same type - if (!isFieldValueReturned(write, writeIndex, readIndex, taCode, findGuardsResult)) - return Assignable; - // prevents that the field is seen with another value - if ( // potentially unsound with method.returnType == state.field.fieldType - // TODO comment it out and look at appearing cases - taCode.stmts.exists(stmt => - stmt.isReturnValue && !isTransitivePredecessor( - writeBB, - cfg.bb(taCode.pcToIndex(stmt.pc)) - ) && - findGuardsResult.forall { // TODO check... - case (indexOfFieldRead, _, _, _) => - !isTransitivePredecessor( - cfg.bb(indexOfFieldRead), - cfg.bb(taCode.pcToIndex(stmt.pc)) - ) - } - ) - ) - return Assignable; - } - - if (state.fieldReadAccessDependee.isEmpty) { - state.fieldReadAccessDependee = - Some(propertyStore(declaredFields(state.field), FieldReadAccessInformation.key)) + val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { + case (writePC, _) => + pathExists(readPC, writePC, tac) } - val fraiEP = state.fieldReadAccessDependee.get - - if (fraiEP.hasUBP && doFieldReadsEscape(fraiEP.ub.accesses.toSeq, method, guardIndex, writeIndex, taCode)) - return Assignable; - - state.checkLazyInit = Some((method, guardIndex, writeIndex, taCode)) - - if (write.value.asVar.definedBy.forall { _ >= 0 } && - dominates(defaultCaseIndex, writeIndex, taCode) && noInterferingExceptions() - ) { - if (method.isSynchronized) - LazilyInitialized - else { - val (indexMonitorEnter, indexMonitorExit) = findMonitors(writeIndex, taCode) - val monitorResultsDefined = indexMonitorEnter.isDefined && indexMonitorExit.isDefined - if (monitorResultsDefined && dominates(indexMonitorEnter.get, readIndex, taCode)) - LazilyInitialized - else - UnsafelyLazilyInitialized - } - } else + if (pathFromReadToSomeWriteExists) Assignable - } - - def doFieldReadsEscape( - reads: Seq[(Int, PC, AccessReceiver, AccessParameter)], - method: Method, - guardIndex: Int, - writeIndex: Int, - taCode: TACode[TACMethodParameter, V] - )(implicit state: AnalysisState): Boolean = boundary { - // prevents reads outside the method - if (reads.exists(r => contextProvider.contextFromId(r._1).method.definedMethod ne method)) - return true; - - var seen: Set[Stmt[V]] = Set.empty - - def doUsesEscape( - pcs: PCs - )(implicit state: AnalysisState): Boolean = { - val cfg = taCode.cfg - - pcs.exists(pc => { - val index = taCode.pcToIndex(pc) - if (index == -1) - break(true); - val stmt = taCode.stmts(index) - - if (stmt.isAssignment) { - stmt.asAssignment.targetVar.usedBy.exists(i => - i == -1 || { - val st = taCode.stmts(i) - if (!seen.contains(st)) { - seen += st - !( - st.isReturnValue || st.isIf || - dominates(guardIndex, i, taCode) && - isTransitivePredecessor(cfg.bb(writeIndex), cfg.bb(i)) || - (st match { - case AssignmentLikeStmt(_, expr) => - (expr.isCompare || expr.isFunctionCall && { - val functionCall = expr.asFunctionCall - state.field.fieldType match { - case ClassType.Byte => functionCall.name == "byteValue" - case ClassType.Short => functionCall.name == "shortValue" - case ClassType.Integer => functionCall.name == "intValue" - case ClassType.Long => functionCall.name == "longValue" - case ClassType.Float => functionCall.name == "floatValue" - case ClassType.Double => functionCall.name == "doubleValue" - case _ => false - } - }) && !doUsesEscape(st.asAssignment.targetVar.usedBy) - case _ => false - }) - ) - } else false - } - ) - } else false - }) - } - - reads.exists(a => doUsesEscape(IntTrieSet(a._2))) - } - - /** - * This method returns the information about catch blocks, throw statements and return nodes - * - * @note It requires still determined taCode - * - * @return The first element of the tuple returns: - * the caught exceptions (the pc of the catch, the exception type, the origin of the caught exception, - * the bb of the caughtException) - * @return The second element of the tuple returns: - * The throw statements: (the pc, the definitionSites, the bb of the throw statement) - * @author Tobias Roth - */ - def findCatchesAndThrows( - tacCode: TACode[TACMethodParameter, V] - ): (List[(Int, IntTrieSet)], List[(Int, IntTrieSet)]) = { - var caughtExceptions: List[(Int, IntTrieSet)] = List.empty - var throwStatements: List[(Int, IntTrieSet)] = List.empty - for (stmt <- tacCode.stmts) { - if (!stmt.isNop) { // to prevent the handling of partially negative pcs of nops - (stmt.astID: @switch) match { - - case CaughtException.ASTID => - val caughtException = stmt.asCaughtException - caughtExceptions = (caughtException.pc, caughtException.origins) :: caughtExceptions - - case Throw.ASTID => - val throwStatement = stmt.asThrow - val throwStatementDefinedBys = throwStatement.exception.asVar.definedBy - throwStatements = (throwStatement.pc, throwStatementDefinedBys) :: throwStatements - - case _ => - } - } - } - (caughtExceptions, throwStatements) - } - - /** - * Searches the closest monitor enter and exit to the field write. - * @return the index of the monitor enter and exit - * @author Tobias Roth - */ - def findMonitors( - fieldWrite: Int, - tacCode: TACode[TACMethodParameter, V] - )(implicit state: State): (Option[Int], Option[Int]) = { - - var result: (Option[Int], Option[Int]) = (None, None) - val startBB = tacCode.cfg.bb(fieldWrite) - var monitorExitQueuedBBs: Set[CFGNode] = startBB.successors - var worklistMonitorExit = getSuccessors(startBB, Set.empty) + else + NonAssignable + } + + override def analyzeNonInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: State): FieldAssignability = { + val assignability = completeLazyInitPatternForNonInitializerRead(context, readPC) + if (assignability == Assignable) + return Assignable; - /** - * checks that a given monitor supports a thread safe lazy initialization. - * Supports two ways of synchronized blocks. - * - * When determining the lazy initialization of a static field, - * it allows only global locks on Foo.class. Independent of which class Foo is. - * - * When determining the lazy initialization of an instance fields, it allows - * synchronized(this) and synchronized(Foo.class). Independent of which class Foo is. - * In case of an instance field the second case is even stronger than synchronized(this). - */ - def checkMonitor(v: V)(implicit state: State): Boolean = { - v.definedBy.forall(definedByIndex => { - if (definedByIndex >= 0) { - tacCode.stmts(definedByIndex) match { - // synchronized(Foo.class) - case Assignment(_, _, _: ClassConst) => true - case _ => false - } + // Completes the analysis of the clone pattern by recognizing unsafe read-write paths on the same field + val pathFromReadToSomeWriteExists = state.nonInitializerWrites(context).exists { + case (writePC, writeReceiver) => + val writeReceiverVar = writeReceiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (writeReceiverVar.isDefined && isSameInstance(tac, receiver.get, writeReceiverVar.get).isNo) { + false } else { - // synchronized(this) - state.field.isNotStatic && IntTrieSet(definedByIndex) == SelfReferenceParameter + pathExists(readPC, writePC, tac) } - }) } - var monitorEnterQueuedBBs: Set[CFGNode] = startBB.predecessors - var worklistMonitorEnter = getPredecessors(startBB, Set.empty) - - // find monitorenter - while (worklistMonitorEnter.nonEmpty) { - val curBB = worklistMonitorEnter.head - worklistMonitorEnter = worklistMonitorEnter.tail - val startPC = curBB.startPC - val endPC = curBB.endPC - var hasNotFoundAnyMonitorYet = true - for (i <- startPC to endPC) { - (tacCode.stmts(i).astID: @switch) match { - case MonitorEnter.ASTID => - val monitorEnter = tacCode.stmts(i).asMonitorEnter - if (checkMonitor(monitorEnter.objRef.asVar)) { - result = (Some(tacCode.pcToIndex(monitorEnter.pc)), result._2) - hasNotFoundAnyMonitorYet = false - } - case _ => - } - } - if (hasNotFoundAnyMonitorYet) { - val predecessor = getPredecessors(curBB, monitorEnterQueuedBBs) - worklistMonitorEnter ++= predecessor - monitorEnterQueuedBBs ++= predecessor - } - } - // find monitorexit - while (worklistMonitorExit.nonEmpty) { - val curBB = worklistMonitorExit.head - - worklistMonitorExit = worklistMonitorExit.tail - val endPC = curBB.endPC - - val cfStmt = tacCode.stmts(endPC) - (cfStmt.astID: @switch) match { - - case MonitorExit.ASTID => - val monitorExit = cfStmt.asMonitorExit - if (checkMonitor(monitorExit.objRef.asVar)) { - result = (result._1, Some(tacCode.pcToIndex(monitorExit.pc))) - } - - case _ => - val successors = getSuccessors(curBB, monitorExitQueuedBBs) - worklistMonitorExit ++= successors - monitorExitQueuedBBs ++= successors - } - } - result - } - - /** - * Finds the indexes of the guarding if-Statements for a lazy initialization, the index of the - * first statement executed if the field does not have its default value and the index of the - * field read used for the guard and the index of the field-read. - */ - def findGuards( - fieldWrite: Int, - defaultValues: Set[Any], - taCode: TACode[TACMethodParameter, V] - )(implicit state: State): List[(Int, Int, Int, Int)] = { - val cfg = taCode.cfg - val code = taCode.stmts - - val startBB = cfg.bb(fieldWrite).asBasicBlock - - var enqueuedBBs: Set[CFGNode] = startBB.predecessors - var worklist: List[BasicBlock] = getPredecessors(startBB, Set.empty) - var seen: Set[BasicBlock] = Set.empty - var result: List[(Int, Int, Int)] = List.empty /* guard pc, true target pc, false target pc */ - - while (worklist.nonEmpty) { - val curBB = worklist.head - worklist = worklist.tail - if (!seen.contains(curBB)) { - seen += curBB - - val endPC = curBB.endPC - - val cfStmt = code(endPC) - (cfStmt.astID: @switch) match { + if (pathFromReadToSomeWriteExists) + Assignable + else + NonAssignable + } + + override def analyzeInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: State): FieldAssignability = { + val assignability = completeLazyInitPatternForInitializerRead() + if (assignability == Assignable) + return Assignable; - case If.ASTID => - val ifStmt = cfStmt.asIf - if (ifStmt.condition.equals(EQ) && curBB != startBB && isGuard( - ifStmt, - defaultValues, - code, - taCode - ) - ) { - result = (endPC, ifStmt.targetStmt, endPC + 1) :: result - } else if (ifStmt.condition.equals(NE) && curBB != startBB && isGuard( - ifStmt, - defaultValues, - code, - taCode - ) - ) { - result = (endPC, endPC + 1, ifStmt.targetStmt) :: result - } else { - if ((cfg.bb(fieldWrite) != cfg.bb(ifStmt.target) || fieldWrite < ifStmt.target) && - isTransitivePredecessor(cfg.bb(fieldWrite), cfg.bb(ifStmt.target)) - ) { - return List.empty // in cases where other if-statements destroy - } - } - val predecessors = getPredecessors(curBB, enqueuedBBs) - worklist ++= predecessors - enqueuedBBs ++= predecessors + val method = context.method.definedMethod + if (state.field.isStatic && method.isConstructor || state.field.isNotStatic && method.isStaticInitializer) + return Assignable; // TODO check this generally above - // Otherwise, we have to ensure that a guard is present for all predecessors - case _ => - val predecessors = getPredecessors(curBB, enqueuedBBs) - worklist ++= predecessors - enqueuedBBs ++= predecessors - } - } + // For instance writes, there might be premature value escapes through arbitrary use sites. + // If we cannot guarantee that all use sites of the written instance are safe, we must soundly abort. + if (state.field.isNotStatic) { + // We only support modifying fields in initializers on newly created instances (which is always "this") + if (receiver.isEmpty || receiver.get.definedBy != SelfReferenceParameter) + return Assignable; + if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC))) + return Assignable; } - var finalResult: List[(Int, Int, Int, Int)] = List.empty - var fieldReadIndex = 0 - result.foreach { case (guardPC, trueTargetPC, falseTargetPC) => - // The field read that defines the value checked by the guard must be used only for the - // guard or directly if the field's value was not the default value - val ifStmt = code(guardPC).asIf - - val expr = - if (ifStmt.leftExpr.isConst) ifStmt.rightExpr - else ifStmt.leftExpr - - val definitions = expr.asVar.definedBy - if (definitions.forall(_ >= 0)) { - - fieldReadIndex = definitions.head - - val fieldReadUses = code(definitions.head).asAssignment.targetVar.usedBy - - val fieldReadUsedCorrectly = - fieldReadUses.forall(use => use == guardPC || use == falseTargetPC) + // Analyzing read-write paths interprocedurally is not supported yet + if (state.initializerReads.exists(_._1.method ne context.method)) + return Assignable; - if (definitions.size == 1 && definitions.head >= 0 && fieldReadUsedCorrectly) { - // Found proper guard - finalResult = (fieldReadIndex, guardPC, trueTargetPC, falseTargetPC) :: finalResult + val pathFromSomeReadToWriteExists = state.initializerReads(context).exists { + case (readPC, readReceiver) => + val readReceiverVar = readReceiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (readReceiverVar.isDefined && isSameInstance(tac, receiver.get, readReceiverVar.get).isNo) { + false + } else { + pathExists(readPC, writePC, tac) } - } - } - finalResult - } - - /** - * Returns all predecessor BasicBlocks of a CFGNode. - */ - def getPredecessors(node: CFGNode, visited: Set[CFGNode]): List[BasicBlock] = { - def getPredecessorsInternal(node: CFGNode, visited: Set[CFGNode]): Iterator[BasicBlock] = { - node.predecessors.iterator.flatMap { currentNode => - if (currentNode.isBasicBlock) { - if (visited.contains(currentNode)) - None - else - Some(currentNode.asBasicBlock) - } else - getPredecessorsInternal(currentNode, visited) - } - } - getPredecessorsInternal(node, visited).toList - } - - /** - * Determines whether a node is a transitive predecessor of another node. - */ - def isTransitivePredecessor(possiblePredecessor: CFGNode, node: CFGNode): Boolean = { - - val visited: mutable.Set[CFGNode] = mutable.Set.empty - - def isTransitivePredecessorInternal(possiblePredecessor: CFGNode, node: CFGNode): Boolean = { - if (possiblePredecessor == node) - true - else if (visited.contains(node)) - false - else { - visited += node - node.predecessors.exists(currentNode => isTransitivePredecessorInternal(possiblePredecessor, currentNode)) - } - } - isTransitivePredecessorInternal(possiblePredecessor, node) - } - - /** - * Returns all successors BasicBlocks of a CFGNode - */ - def getSuccessors(node: CFGNode, visited: Set[CFGNode]): List[BasicBlock] = { - def getSuccessorsInternal(node: CFGNode, visited: Set[CFGNode]): Iterator[BasicBlock] = { - node.successors.iterator flatMap { currentNode => - if (currentNode.isBasicBlock) - if (visited.contains(currentNode)) None - else Some(currentNode.asBasicBlock) - else getSuccessors(currentNode, visited) - } } - getSuccessorsInternal(node, visited).toList - } - - /** - * Checks if an expression is a field read of the currently analyzed field. - * For instance fields, the read must be on the `this` reference. - */ - def isReadOfCurrentField( - expr: Expr[V], - tacCode: TACode[TACMethodParameter, V], - index: Int - )(implicit state: State): Boolean = { - def isExprReadOfCurrentField: Int => Boolean = - exprIndex => - exprIndex == index || - exprIndex >= 0 && isReadOfCurrentField( - tacCode.stmts(exprIndex).asAssignment.expr, - tacCode, - exprIndex - ) - (expr.astID: @switch) match { - case GetField.ASTID => - val objRefDefinition = expr.asGetField.objRef.asVar.definedBy - if (objRefDefinition != SelfReferenceParameter) false - else expr.asGetField.resolveField(using project).contains(state.field) - case GetStatic.ASTID => expr.asGetStatic.resolveField(using project).contains(state.field) - case PrimitiveTypecastExpr.ASTID => false + if (pathFromSomeReadToWriteExists) + Assignable + else + NonAssignable + } + + override def analyzeNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: State): FieldAssignability = { + if (state.field.isStatic || receiver.isDefined && receiver.get.definedBy == SelfReferenceParameter) { + completeLazyInitPatternForNonInitializerWrite(context, tac, writePC) + } else if (receiver.isDefined) { + // Check for a clone pattern: Must be on a fresh instance (i.e. cannot be "this" or a parameter) ... + if (receiver.get.definedBy.exists(_ <= OriginOfThis) || receiver.get.definedBy.size > 1) + return Assignable; + val defSite = receiver.get.definedBy.head + if (!tac.stmts(defSite).asAssignment.expr.isNew) + return Assignable; - case Compare.ASTID => - val leftExpr = expr.asCompare.left - val rightExpr = expr.asCompare.right - leftExpr.asVar.definedBy - .forall(index => index >= 0 && tacCode.stmts(index).asAssignment.expr.isConst) && - rightExpr.asVar.definedBy.forall(isExprReadOfCurrentField) || - rightExpr.asVar.definedBy - .forall(index => index >= 0 && tacCode.stmts(index).asAssignment.expr.isConst) && - leftExpr.asVar.definedBy.forall(isExprReadOfCurrentField) + // ... free of dangerous uses, ... + val useSites = tac.stmts(defSite).asAssignment.targetVar.usedBy + if (!useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC))) + return Assignable; - case VirtualFunctionCall.ASTID => - val functionCall = expr.asVirtualFunctionCall - val fieldType = state.field.fieldType - functionCall.params.isEmpty && ( - fieldType match { - case ClassType.Byte => functionCall.name == "byteValue" - case ClassType.Short => functionCall.name == "shortValue" - case ClassType.Integer => functionCall.name == "intValue" - case ClassType.Long => functionCall.name == "longValue" - case ClassType.Float => functionCall.name == "floatValue" - case ClassType.Double => functionCall.name == "doubleValue" - case _ => false + // ... and not contain read-write paths, whenever their receivers cannot be proven to be disjoint + val pathFromSomeReadToWriteExists = state.nonInitializerReads(context).exists { + case (readPC, readReceiver) => + val readReceiverVar = readReceiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (readReceiverVar.isDefined && isSameInstance(tac, receiver.get, readReceiverVar.get).isNo) { + false + } else { + pathExists(readPC, writePC, tac) } - ) && functionCall.receiver.asVar.definedBy.forall(isExprReadOfCurrentField) + } - case _ => false - } + if (pathFromSomeReadToWriteExists) + Assignable + else + NonAssignable + } else + Assignable } /** - * Determines if an if-Statement is actually a guard for the current field, i.e. it compares - * the current field to the default value. + * Identifies easy cases of definitively answering instance equality, considering that (at least) as long as DU-UD + * chains are not collapsed in TAC one cannot be sure that two instances are disjoint, i.e. that they are different. */ - def isGuard( - ifStmt: If[V], - defaultValues: Set[Any], - code: Array[Stmt[V]], - tacCode: TACode[TACMethodParameter, V] - )(implicit state: State): Boolean = { - - def isDefaultConst(expr: Expr[V]): Boolean = { - - if (expr.isVar) { - val defSites = expr.asVar.definedBy - defSites.size == 1 && defSites.forall(_ >= 0) && - defSites.forall(defSite => isDefaultConst(code(defSite).asAssignment.expr)) - } else { - // default value check - expr.isIntConst && defaultValues.contains(expr.asIntConst.value) || - expr.isFloatConst && defaultValues.contains(expr.asFloatConst.value) || - expr.isDoubleConst && defaultValues.contains(expr.asDoubleConst.value) || - expr.isLongConst && defaultValues.contains(expr.asLongConst.value) || - expr.isStringConst && defaultValues.contains(expr.asStringConst.value) || - expr.isNullExpr && defaultValues.contains(null) - } - } - - /** - * Checks whether the non-constant expression of the if-Statement is a read of the current - * field. - */ - def isGuardInternal( - expr: V, - tacCode: TACode[TACMethodParameter, V] - ): Boolean = { - expr.definedBy forall { index => - if (index < 0) - false // If the value is from a parameter, this can not be the guard - else { - val expression = code(index).asAssignment.expr - // in case of Integer etc.... .initValue() - if (expression.isVirtualFunctionCall) { - val virtualFunctionCall = expression.asVirtualFunctionCall - virtualFunctionCall.receiver.asVar.definedBy.forall(receiverDefSite => - receiverDefSite >= 0 && - isReadOfCurrentField(code(receiverDefSite).asAssignment.expr, tacCode, index) - ) - } else - isReadOfCurrentField(expression, tacCode, index) - } + private def isSameInstance(tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V): Answer = { + def isFresh(value: V): Boolean = { + value.definedBy.size == 1 && { + val defSite = value.definedBy.head + defSite >= 0 && tac.stmts(defSite).asAssignment.expr.isNew } } - // Special handling for these types needed because of compare function in bytecode - def hasFloatDoubleOrLongType(fieldType: FieldType): Boolean = - fieldType.isFloatType || fieldType.isDoubleType || fieldType.isLongType - - if (ifStmt.rightExpr.isVar && hasFloatDoubleOrLongType(state.field.fieldType) && - ifStmt.rightExpr.asVar.definedBy.head > 0 && - tacCode.stmts(ifStmt.rightExpr.asVar.definedBy.head).asAssignment.expr.isCompare - ) { - - val left = - tacCode.stmts(ifStmt.rightExpr.asVar.definedBy.head).asAssignment.expr.asCompare.left.asVar - val right = - tacCode.stmts(ifStmt.rightExpr.asVar.definedBy.head).asAssignment.expr.asCompare.right.asVar - val leftExpr = tacCode.stmts(left.definedBy.head).asAssignment.expr - val rightExpr = tacCode.stmts(right.definedBy.head).asAssignment.expr - - if (leftExpr.isGetField || leftExpr.isGetStatic) isDefaultConst(rightExpr) - else (rightExpr.isGetField || rightExpr.isGetStatic) && isDefaultConst(leftExpr) - - } else if (ifStmt.leftExpr.isVar && ifStmt.rightExpr.isVar && ifStmt.leftExpr.asVar.definedBy.head >= 0 && - ifStmt.rightExpr.asVar.definedBy.head >= 0 && - hasFloatDoubleOrLongType(state.field.fieldType) && tacCode - .stmts(ifStmt.leftExpr.asVar.definedBy.head) - .asAssignment - .expr - .isCompare && - ifStmt.leftExpr.isVar && ifStmt.rightExpr.isVar + // The two given instances may be categorized into: 1. fresh, 2. this, 3. easy case of formal parameter, + // 4. none of the above. If both instances fall into some category 1-3, but not into the same, they are secured + // to be disjoint. + val instanceInfo = Seq( + (isFresh(firstVar), isFresh(secondVar)), + (firstVar.definedBy == SelfReferenceParameter, secondVar.definedBy == SelfReferenceParameter), + (firstVar.definedBy.forall(_ < OriginOfThis), secondVar.definedBy.forall(_ < OriginOfThis)) + ) + if (instanceInfo.contains((true, false)) && + instanceInfo.contains((false, true)) && + !instanceInfo.contains(true, true) ) { - - val left = - tacCode.stmts(ifStmt.leftExpr.asVar.definedBy.head).asAssignment.expr.asCompare.left.asVar - val right = - tacCode.stmts(ifStmt.leftExpr.asVar.definedBy.head).asAssignment.expr.asCompare.right.asVar - val leftExpr = tacCode.stmts(left.definedBy.head).asAssignment.expr - val rightExpr = tacCode.stmts(right.definedBy.head).asAssignment.expr - - if (leftExpr.isGetField || leftExpr.isGetStatic) isDefaultConst(rightExpr) - else (rightExpr.isGetField || rightExpr.isGetStatic) && isDefaultConst(leftExpr) - - } else if (ifStmt.rightExpr.isVar && isDefaultConst(ifStmt.leftExpr)) { - isGuardInternal(ifStmt.rightExpr.asVar, tacCode) - } else if (ifStmt.leftExpr.isVar && isDefaultConst(ifStmt.rightExpr)) { - isGuardInternal(ifStmt.leftExpr.asVar, tacCode) - } else false + No + } else if (firstVar.definedBy.intersect(secondVar.definedBy).nonEmpty) + Yes + else + Unknown } /** - * Checks that the returned value is definitely read from the field. + * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not + * make the field assignable in combination with the given write itself. */ - def isFieldValueReturned( - write: FieldWriteAccessStmt[V], - writeIndex: Int, - readIndex: Int, - taCode: TACode[TACMethodParameter, V], - guardIndexes: List[(Int, Int, Int, Int)] - )(implicit state: State): Boolean = { - - def isSimpleReadOfField(expr: Expr[V]) = - expr.astID match { - - case GetField.ASTID => - val objRefDefinition = expr.asGetField.objRef.asVar.definedBy - if (objRefDefinition != SelfReferenceParameter) - false - else - expr.asGetField.resolveField(using project).contains(state.field) - - case GetStatic.ASTID => expr.asGetStatic.resolveField(using project).contains(state.field) - - case _ => false - } - - taCode.stmts.forall { stmt => - !stmt.isReturnValue || { - - val returnValueDefs = stmt.asReturnValue.expr.asVar.definedBy - val assignedValueDefSite = write.value.asVar.definedBy - returnValueDefs.forall(_ >= 0) && { - if (returnValueDefs.size == 1 && returnValueDefs.head != readIndex) { - val expr = taCode.stmts(returnValueDefs.head).asAssignment.expr - isSimpleReadOfField(expr) && guardIndexes.exists { - case (_, guardIndex, defaultCase, _) => - dominates(guardIndex, returnValueDefs.head, taCode) && - (!dominates(defaultCase, returnValueDefs.head, taCode) || - dominates(writeIndex, returnValueDefs.head, taCode)) - } - } // The field is either read before the guard and returned or - // the value assigned to the field is returned - else { - returnValueDefs.size == 2 && assignedValueDefSite.size == 1 && - returnValueDefs.contains(readIndex) && { - returnValueDefs.contains(assignedValueDefSite.head) || { - val potentiallyReadIndex = returnValueDefs.filter(_ != readIndex).head - val expr = taCode.stmts(potentiallyReadIndex).asAssignment.expr - isSimpleReadOfField(expr) && - guardIndexes.exists { - case (_, guardIndex, defaultCase, _) => - dominates(guardIndex, potentiallyReadIndex, taCode) && - (!dominates(defaultCase, returnValueDefs.head, taCode) || - dominates(writeIndex, returnValueDefs.head, taCode)) - } - } - } - } - } - } - } + private def isWrittenInstanceUseSiteSafe(tac: TACode[TACMethodParameter, V], writePC: Int)(use: Int): Boolean = { + val stmt = tac.stmts(use) + + // We consider the access safe when ... + stmt.pc == writePC || // ... the use site is a known write OR ... + // ... we easily identify the use site to be a field update OR ... + // IMPROVE: Can we use field access information to care about reflective accesses here? + stmt.isFieldWriteAccessStmt || + // ... we identify easy instances of field reads (interaction with potentially disruptive reads + // is checked separately; note that this inference relies on a flat TAC) OR ... + stmt.isAssignment && stmt.asAssignment.expr.isGetField || + // ... we easily identify the use site to initialize an object OR ... TODO check that this is sound + stmt.isMethodCall && stmt.asMethodCall.name == "" || + // ... the statement is irrelevant, since there is no instance where the write did not take effect yet + !pathExists(stmt.pc, writePC, tac) } - } -trait L2FieldAssignabilityAnalysisScheduler extends AbstractFieldAssignabilityAnalysisScheduler { - override def uses: Set[PropertyBounds] = super.uses ++ PropertyBounds.ubs(FieldReadAccessInformation) +object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) } -/** - * Executor for the eager field assignability analysis. - */ -object EagerL2FieldAssignabilityAnalysis extends L2FieldAssignabilityAnalysisScheduler - with BasicFPCFEagerAnalysisScheduler { - - override def derivesEagerly: Set[PropertyBounds] = Set(derivedProperty) - - override def derivesCollaboratively: Set[PropertyBounds] = Set.empty - - override final def start(p: SomeProject, ps: PropertyStore, unused: Null): FPCFAnalysis = { - val analysis = new L2FieldAssignabilityAnalysis(p) - val fields = p.allFields - ps.scheduleEagerComputationsForEntities(fields)(analysis.determineFieldAssignability) - analysis - } -} - -/** - * Executor for the lazy field assignability analysis. - */ -object LazyL2FieldAssignabilityAnalysis extends L2FieldAssignabilityAnalysisScheduler - with BasicFPCFLazyAnalysisScheduler { - - override def derivesLazily: Some[PropertyBounds] = Some(derivedProperty) - - override final def register( - p: SomeProject, - ps: PropertyStore, - unused: Null - ): FPCFAnalysis = { - val analysis = new L2FieldAssignabilityAnalysis(p) - ps.registerLazyPropertyComputation( - FieldAssignability.key, - analysis.doDetermineFieldAssignability - ) - analysis - } +object LazyL2FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala new file mode 100644 index 0000000000..a98d2e0a79 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala @@ -0,0 +1,802 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability + +import scala.annotation.switch + +import scala.collection.mutable +import scala.util.boundary +import scala.util.boundary.break + +import org.opalj.RelationalOperators.EQ +import org.opalj.RelationalOperators.NE +import org.opalj.br.BooleanType +import org.opalj.br.ByteType +import org.opalj.br.CharType +import org.opalj.br.ClassType +import org.opalj.br.DoubleType +import org.opalj.br.FieldType +import org.opalj.br.FloatType +import org.opalj.br.IntegerType +import org.opalj.br.LongType +import org.opalj.br.PC +import org.opalj.br.PCs +import org.opalj.br.ReferenceType +import org.opalj.br.ShortType +import org.opalj.br.analyses.SomeProject +import org.opalj.br.cfg.BasicBlock +import org.opalj.br.cfg.CFGNode +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.immutability.Assignable +import org.opalj.br.fpcf.properties.immutability.FieldAssignability +import org.opalj.br.fpcf.properties.immutability.LazilyInitialized +import org.opalj.br.fpcf.properties.immutability.NonAssignable +import org.opalj.br.fpcf.properties.immutability.UnsafelyLazilyInitialized +import org.opalj.collection.immutable.IntTrieSet +import org.opalj.tac.CaughtException +import org.opalj.tac.ClassConst +import org.opalj.tac.Compare +import org.opalj.tac.Expr +import org.opalj.tac.FieldWriteAccessStmt +import org.opalj.tac.GetField +import org.opalj.tac.GetStatic +import org.opalj.tac.If +import org.opalj.tac.MonitorEnter +import org.opalj.tac.MonitorExit +import org.opalj.tac.PrimitiveTypecastExpr +import org.opalj.tac.SelfReferenceParameter +import org.opalj.tac.Stmt +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.Throw +import org.opalj.tac.VirtualFunctionCall + +/** + * Determines whether a field write access corresponds to a lazy initialization of the field. + * + * @note Requires that the 3-address code's expressions are not deeply nested. + * + * @author Tobias Roth + * @author Dominik Helm + * @author Florian Kübler + * @author Michael Eichberg + * @author Maximilian Rüsch + */ +trait LazyInitializationAnalysis private[fieldassignability] + extends AbstractFieldAssignabilityAnalysis + with FPCFAnalysis { + + trait LazyInitializationAnalysisState extends AbstractFieldAssignabilityAnalysisState { + // context : index of guard statement : index of write statement : TAC + var potentialLazyInit: Option[(Context, Int, Int, TACode[TACMethodParameter, V])] = None + } + + override type AnalysisState <: LazyInitializationAnalysisState + + val project: SomeProject + val considerLazyInitialization: Boolean = + project.config.getBoolean( + "org.opalj.fpcf.analyses.L2FieldAssignabilityAnalysis.considerLazyInitialization" + ) + + /** + * Returns the initialization value of a given type. + */ + private def fieldDefaultValues(implicit state: AnalysisState): Set[Any] = state.field.fieldType match { + case FloatType | ClassType.Float => Set(0.0f) + case DoubleType | ClassType.Double => Set(0.0d) + case LongType | ClassType.Long => Set(0L) + case CharType | ClassType.Character => Set('\u0000') + case BooleanType | ClassType.Boolean => Set(false) + case IntegerType | ClassType.Integer | ByteType | ClassType.Byte | ShortType | ClassType.Short => Set(0) + case ClassType.String => Set("", null) + case _: ReferenceType => Set(null) + } + + def completeLazyInitPatternForInitializerRead()(implicit state: AnalysisState): FieldAssignability = { + if (state.potentialLazyInit.isEmpty) + NonAssignable + else + Assignable + } + + def completeLazyInitPatternForNonInitializerRead( + context: Context, + readPC: Int + )(implicit state: AnalysisState): FieldAssignability = { + // No lazy init pattern exists, or it was not discovered yet + if (state.potentialLazyInit.isEmpty) + return NonAssignable; + + val (lazyInitContext, guardIndex, writeIndex, tac) = state.potentialLazyInit.get + // We only support lazy initialization patterns fully contained within one method, but different contexts of the + // same method are fine. + if (context.method ne lazyInitContext.method) + return Assignable; + if (context.id != lazyInitContext.id) + return NonAssignable; + + if (doFieldReadsEscape(Set(readPC), guardIndex, writeIndex, tac)) + Assignable + else + NonAssignable + } + + def completeLazyInitPatternForInitializerWrite()(implicit state: AnalysisState): FieldAssignability = { + if (state.potentialLazyInit.isEmpty) + NonAssignable + else + Assignable + } + + /** + * To be called when a non-initializer write on either a static field or a 'this' reference is discovered. + */ + def completeLazyInitPatternForNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC + )(implicit state: AnalysisState): FieldAssignability = { + // A lazy initialization pattern does not allow initializing a field regularly + if (state.initializerWrites.nonEmpty) + return Assignable; + + // E.g. multiple lazy initialization patterns cannot be supported in a collaborative setting + if (state.nonInitializerWrites.iterator.distinctBy(_._1.method).size > 1) + return Assignable; + + // We do not support multiple-write lazy initializations yet + if (state.nonInitializerWrites(context).size > 1) + return Assignable; + + val method = context.method.definedMethod + val writeIndex = tac.pcToIndex(writePC) + val cfg = tac.cfg + val writeBB = cfg.bb(writeIndex).asBasicBlock + + // We only support lazy initialization using direct field writes + if (!tac.stmts(writeIndex).isFieldWriteAccessStmt) + return Assignable; + val writeStmt = tac.stmts(writeIndex).asFieldWriteAccessStmt + + val resultCatchesAndThrows = findCatchesAndThrows(tac) + val findGuardsResult = findGuards(writeIndex, tac) + // no guard -> no lazy initialization + if (findGuardsResult.isEmpty) + return Assignable; + + val (readIndex, guardIndex, defaultCaseIndex, elseCaseIndex) = findGuardsResult.head + + // The field has to be written when the guard is in the default-case branch + if (!dominates(defaultCaseIndex, writeIndex, tac)) + return Assignable; + + val elseBB = cfg.bb(elseCaseIndex) + + // prevents wrong control flow + if (isTransitivePredecessor(elseBB, writeBB)) + return Assignable; + + if (method.returnType == state.field.fieldType) { + // prevents that another value than the field value is returned with the same type + if (!isFieldValueReturned(writeStmt, writeIndex, readIndex, tac, findGuardsResult)) + return Assignable; + // prevents that the field is seen with another value + if ( // potentially unsound with method.returnType == state.field.fieldType + // TODO comment it out and look at appearing cases + tac.stmts.exists(stmt => + stmt.isReturnValue && !isTransitivePredecessor( + writeBB, + cfg.bb(tac.pcToIndex(stmt.pc)) + ) && + findGuardsResult.forall { // TODO check... + case (indexOfFieldRead, _, _, _) => + !isTransitivePredecessor( + cfg.bb(indexOfFieldRead), + cfg.bb(tac.pcToIndex(stmt.pc)) + ) + } + ) + ) + return Assignable; + } + + // A lazy init does not allow considering reads outside the lazy initialization method, effectively also + // preventing analysis multi-lazy-init patterns. + if (state.initializerReads.nonEmpty || state.nonInitializerReads.exists(_._1.method ne context.method)) + return Assignable; + + if (doFieldReadsEscape(state.nonInitializerReads(context).map(_._1), guardIndex, writeIndex, tac)) + return Assignable; + + state.potentialLazyInit = Some((context, guardIndex, writeIndex, tac)) + + /** + * Determines whether all caught exceptions are thrown afterwards + */ + def noInterferingExceptions(): Boolean = + resultCatchesAndThrows._1.forall { + case (catchPC, originsCaughtException) => + resultCatchesAndThrows._2.exists { + case (throwPC, throwDefinitionSites) => + dominates(tac.pcToIndex(catchPC), tac.pcToIndex(throwPC), tac) && + throwDefinitionSites == originsCaughtException // throwing and catching same exceptions + } + } + + if (writeStmt.value.asVar.definedBy.forall(_ >= 0) && + dominates(defaultCaseIndex, writeIndex, tac) && + noInterferingExceptions() + ) { + if (method.isSynchronized) + LazilyInitialized + else { + val (indexMonitorEnter, indexMonitorExit) = findMonitors(writeIndex, tac) + val monitorResultsDefined = indexMonitorEnter.isDefined && indexMonitorExit.isDefined + if (monitorResultsDefined && dominates(indexMonitorEnter.get, readIndex, tac)) + LazilyInitialized + else + UnsafelyLazilyInitialized + } + } else + Assignable + } + + private def doFieldReadsEscape( + reads: Set[PC], + guardIndex: Int, + writeIndex: Int, + tac: TACode[TACMethodParameter, V] + )(implicit state: AnalysisState): Boolean = boundary { + var seen: Set[Stmt[V]] = Set.empty + + def doUsesEscape( + pcs: PCs + )(implicit state: AnalysisState): Boolean = { + val cfg = tac.cfg + + pcs.exists(pc => { + val index = tac.pcToIndex(pc) + if (index == -1) + break(true); + val stmt = tac.stmts(index) + + if (stmt.isAssignment) { + stmt.asAssignment.targetVar.usedBy.exists(i => + i == -1 || { + val st = tac.stmts(i) + if (!seen.contains(st)) { + seen += st + !( + st.isReturnValue || st.isIf || + dominates(guardIndex, i, tac) && + isTransitivePredecessor(cfg.bb(writeIndex), cfg.bb(i)) || + (st match { + case AssignmentLikeStmt(_, expr) => + (expr.isCompare || expr.isFunctionCall && { + val functionCall = expr.asFunctionCall + state.field.fieldType match { + case ClassType.Byte => functionCall.name == "byteValue" + case ClassType.Short => functionCall.name == "shortValue" + case ClassType.Integer => functionCall.name == "intValue" + case ClassType.Long => functionCall.name == "longValue" + case ClassType.Float => functionCall.name == "floatValue" + case ClassType.Double => functionCall.name == "doubleValue" + case _ => false + } + }) && !doUsesEscape(st.asAssignment.targetVar.usedBy) + case _ => false + }) + ) + } else false + } + ) + } else false + }) + } + + reads.exists { pc => doUsesEscape(IntTrieSet(pc)) } + } + + /** + * This method returns the information about catch blocks, throw statements and return nodes + * + * @note It requires still determined taCode + * + * @return The first element of the tuple returns: + * the caught exceptions (the pc of the catch, the exception type, the origin of the caught exception, + * the bb of the caughtException) + * @return The second element of the tuple returns: + * The throw statements: (the pc, the definitionSites, the bb of the throw statement) + * @author Tobias Roth + */ + private def findCatchesAndThrows( + tacCode: TACode[TACMethodParameter, V] + ): (List[(Int, IntTrieSet)], List[(Int, IntTrieSet)]) = { + var caughtExceptions: List[(Int, IntTrieSet)] = List.empty + var throwStatements: List[(Int, IntTrieSet)] = List.empty + for (stmt <- tacCode.stmts) { + if (!stmt.isNop) { // to prevent the handling of partially negative pcs of nops + (stmt.astID: @switch) match { + + case CaughtException.ASTID => + val caughtException = stmt.asCaughtException + caughtExceptions = (caughtException.pc, caughtException.origins) :: caughtExceptions + + case Throw.ASTID => + val throwStatement = stmt.asThrow + val throwStatementDefinedBys = throwStatement.exception.asVar.definedBy + throwStatements = (throwStatement.pc, throwStatementDefinedBys) :: throwStatements + + case _ => + } + } + } + (caughtExceptions, throwStatements) + } + + /** + * Searches the closest monitor enter and exit to the field write. + * @return the index of the monitor enter and exit + * @author Tobias Roth + */ + private def findMonitors( + writeIndex: Int, + tac: TACode[TACMethodParameter, V] + )(implicit state: LazyInitializationAnalysisState): (Option[Int], Option[Int]) = { + + var result: (Option[Int], Option[Int]) = (None, None) + val startBB = tac.cfg.bb(writeIndex) + var monitorExitQueuedBBs: Set[CFGNode] = startBB.successors + var worklistMonitorExit = getSuccessors(startBB, Set.empty) + + /** + * checks that a given monitor supports a thread safe lazy initialization. + * Supports two ways of synchronized blocks. + * + * When determining the lazy initialization of a static field, + * it allows only global locks on Foo.class. Independent of which class Foo is. + * + * When determining the lazy initialization of an instance fields, it allows + * synchronized(this) and synchronized(Foo.class). Independent of which class Foo is. + * In case of an instance field the second case is even stronger than synchronized(this). + */ + def checkMonitor(v: V)(implicit state: LazyInitializationAnalysisState): Boolean = { + v.definedBy.forall(definedByIndex => { + if (definedByIndex >= 0) { + tac.stmts(definedByIndex) match { + // synchronized(Foo.class) + case Assignment(_, _, _: ClassConst) => true + case _ => false + } + } else { + // synchronized(this) + state.field.isNotStatic && IntTrieSet(definedByIndex) == SelfReferenceParameter + } + }) + } + + var monitorEnterQueuedBBs: Set[CFGNode] = startBB.predecessors + var worklistMonitorEnter = getPredecessors(startBB, Set.empty) + + // find monitorenter + while (worklistMonitorEnter.nonEmpty) { + val curBB = worklistMonitorEnter.head + worklistMonitorEnter = worklistMonitorEnter.tail + val startPC = curBB.startPC + val endPC = curBB.endPC + var hasNotFoundAnyMonitorYet = true + for (i <- startPC to endPC) { + (tac.stmts(i).astID: @switch) match { + case MonitorEnter.ASTID => + val monitorEnter = tac.stmts(i).asMonitorEnter + if (checkMonitor(monitorEnter.objRef.asVar)) { + result = (Some(tac.pcToIndex(monitorEnter.pc)), result._2) + hasNotFoundAnyMonitorYet = false + } + case _ => + } + } + if (hasNotFoundAnyMonitorYet) { + val predecessor = getPredecessors(curBB, monitorEnterQueuedBBs) + worklistMonitorEnter ++= predecessor + monitorEnterQueuedBBs ++= predecessor + } + } + // find monitorexit + while (worklistMonitorExit.nonEmpty) { + val curBB = worklistMonitorExit.head + + worklistMonitorExit = worklistMonitorExit.tail + val endPC = curBB.endPC + + val cfStmt = tac.stmts(endPC) + (cfStmt.astID: @switch) match { + + case MonitorExit.ASTID => + val monitorExit = cfStmt.asMonitorExit + if (checkMonitor(monitorExit.objRef.asVar)) { + result = (result._1, Some(tac.pcToIndex(monitorExit.pc))) + } + + case _ => + val successors = getSuccessors(curBB, monitorExitQueuedBBs) + worklistMonitorExit ++= successors + monitorExitQueuedBBs ++= successors + } + } + result + } + + /** + * Finds the indexes of the guarding if-Statements for a lazy initialization, the index of the + * first statement executed if the field does not have its default value and the index of the + * field read used for the guard and the index of the field-read. + */ + private def findGuards( + fieldWrite: Int, + taCode: TACode[TACMethodParameter, V] + )(implicit state: AnalysisState): List[(Int, Int, Int, Int)] = { + val cfg = taCode.cfg + val code = taCode.stmts + + val startBB = cfg.bb(fieldWrite).asBasicBlock + + var enqueuedBBs: Set[CFGNode] = startBB.predecessors + var worklist: List[BasicBlock] = getPredecessors(startBB, Set.empty) + var seen: Set[BasicBlock] = Set.empty + var result: List[(Int, Int, Int)] = List.empty /* guard pc, true target pc, false target pc */ + + while (worklist.nonEmpty) { + val curBB = worklist.head + worklist = worklist.tail + if (!seen.contains(curBB)) { + seen += curBB + + val endPC = curBB.endPC + + val cfStmt = code(endPC) + (cfStmt.astID: @switch) match { + + case If.ASTID => + val ifStmt = cfStmt.asIf + if (ifStmt.condition.equals(EQ) && curBB != startBB && isGuard( + ifStmt, + fieldDefaultValues, + code, + taCode + ) + ) { + result = (endPC, ifStmt.targetStmt, endPC + 1) :: result + } else if (ifStmt.condition.equals(NE) && curBB != startBB && isGuard( + ifStmt, + fieldDefaultValues, + code, + taCode + ) + ) { + result = (endPC, endPC + 1, ifStmt.targetStmt) :: result + } else { + if ((cfg.bb(fieldWrite) != cfg.bb(ifStmt.target) || fieldWrite < ifStmt.target) && + isTransitivePredecessor(cfg.bb(fieldWrite), cfg.bb(ifStmt.target)) + ) { + return List.empty // in cases where other if-statements destroy + } + } + val predecessors = getPredecessors(curBB, enqueuedBBs) + worklist ++= predecessors + enqueuedBBs ++= predecessors + + // Otherwise, we have to ensure that a guard is present for all predecessors + case _ => + val predecessors = getPredecessors(curBB, enqueuedBBs) + worklist ++= predecessors + enqueuedBBs ++= predecessors + } + } + + } + + var finalResult: List[(Int, Int, Int, Int)] = List.empty + var fieldReadIndex = 0 + result.foreach { case (guardPC, trueTargetPC, falseTargetPC) => + // The field read that defines the value checked by the guard must be used only for the + // guard or directly if the field's value was not the default value + val ifStmt = code(guardPC).asIf + + val expr = + if (ifStmt.leftExpr.isConst) ifStmt.rightExpr + else ifStmt.leftExpr + + val definitions = expr.asVar.definedBy + if (definitions.forall(_ >= 0)) { + + fieldReadIndex = definitions.head + + val fieldReadUses = code(definitions.head).asAssignment.targetVar.usedBy + + val fieldReadUsedCorrectly = + fieldReadUses.forall(use => use == guardPC || use == falseTargetPC) + + if (definitions.size == 1 && definitions.head >= 0 && fieldReadUsedCorrectly) { + // Found proper guard + finalResult = (fieldReadIndex, guardPC, trueTargetPC, falseTargetPC) :: finalResult + } + } + } + finalResult + } + + /** + * Returns all predecessor BasicBlocks of a CFGNode. + */ + private def getPredecessors(node: CFGNode, visited: Set[CFGNode]): List[BasicBlock] = { + def getPredecessorsInternal(node: CFGNode, visited: Set[CFGNode]): Iterator[BasicBlock] = { + node.predecessors.iterator.flatMap { currentNode => + if (currentNode.isBasicBlock) { + if (visited.contains(currentNode)) + None + else + Some(currentNode.asBasicBlock) + } else + getPredecessorsInternal(currentNode, visited) + } + } + getPredecessorsInternal(node, visited).toList + } + + /** + * Determines whether a node is a transitive predecessor of another node. + */ + private def isTransitivePredecessor(possiblePredecessor: CFGNode, node: CFGNode): Boolean = { + val visited: mutable.Set[CFGNode] = mutable.Set.empty + def isTransitivePredecessorInternal(possiblePredecessor: CFGNode, node: CFGNode): Boolean = { + if (possiblePredecessor == node) + true + else if (visited.contains(node)) + false + else { + visited += node + node.predecessors.exists(currentNode => isTransitivePredecessorInternal(possiblePredecessor, currentNode)) + } + } + isTransitivePredecessorInternal(possiblePredecessor, node) + } + + /** + * Returns all successors BasicBlocks of a CFGNode + */ + private def getSuccessors(node: CFGNode, visited: Set[CFGNode]): List[BasicBlock] = { + def getSuccessorsInternal(node: CFGNode, visited: Set[CFGNode]): Iterator[BasicBlock] = { + node.successors.iterator flatMap { currentNode => + if (currentNode.isBasicBlock) + if (visited.contains(currentNode)) None + else Some(currentNode.asBasicBlock) + else getSuccessors(currentNode, visited) + } + } + getSuccessorsInternal(node, visited).toList + } + + /** + * Checks if an expression is a field read of the currently analyzed field. + * For instance fields, the read must be on the `this` reference. + */ + private def isReadOfCurrentField( + expr: Expr[V], + tacCode: TACode[TACMethodParameter, V], + index: Int + )(implicit state: LazyInitializationAnalysisState): Boolean = { + def isExprReadOfCurrentField: Int => Boolean = + exprIndex => + exprIndex == index || + exprIndex >= 0 && isReadOfCurrentField( + tacCode.stmts(exprIndex).asAssignment.expr, + tacCode, + exprIndex + ) + (expr.astID: @switch) match { + case GetField.ASTID => + val objRefDefinition = expr.asGetField.objRef.asVar.definedBy + if (objRefDefinition != SelfReferenceParameter) false + else expr.asGetField.resolveField(using project).contains(state.field) + + case GetStatic.ASTID => expr.asGetStatic.resolveField(using project).contains(state.field) + case PrimitiveTypecastExpr.ASTID => false + + case Compare.ASTID => + val leftExpr = expr.asCompare.left + val rightExpr = expr.asCompare.right + leftExpr.asVar.definedBy + .forall(index => index >= 0 && tacCode.stmts(index).asAssignment.expr.isConst) && + rightExpr.asVar.definedBy.forall(isExprReadOfCurrentField) || + rightExpr.asVar.definedBy + .forall(index => index >= 0 && tacCode.stmts(index).asAssignment.expr.isConst) && + leftExpr.asVar.definedBy.forall(isExprReadOfCurrentField) + + case VirtualFunctionCall.ASTID => + val functionCall = expr.asVirtualFunctionCall + val fieldType = state.field.fieldType + functionCall.params.isEmpty && ( + fieldType match { + case ClassType.Byte => functionCall.name == "byteValue" + case ClassType.Short => functionCall.name == "shortValue" + case ClassType.Integer => functionCall.name == "intValue" + case ClassType.Long => functionCall.name == "longValue" + case ClassType.Float => functionCall.name == "floatValue" + case ClassType.Double => functionCall.name == "doubleValue" + case _ => false + } + ) && functionCall.receiver.asVar.definedBy.forall(isExprReadOfCurrentField) + + case _ => false + } + } + + /** + * Determines if an if-Statement is actually a guard for the current field, i.e. it compares + * the current field to the default value. + */ + private def isGuard( + ifStmt: If[V], + defaultValues: Set[Any], + code: Array[Stmt[V]], + tacCode: TACode[TACMethodParameter, V] + )(implicit state: AnalysisState): Boolean = { + + def isDefaultConst(expr: Expr[V]): Boolean = { + + if (expr.isVar) { + val defSites = expr.asVar.definedBy + defSites.size == 1 && defSites.forall(_ >= 0) && + defSites.forall(defSite => isDefaultConst(code(defSite).asAssignment.expr)) + } else { + // default value check + expr.isIntConst && defaultValues.contains(expr.asIntConst.value) || + expr.isFloatConst && defaultValues.contains(expr.asFloatConst.value) || + expr.isDoubleConst && defaultValues.contains(expr.asDoubleConst.value) || + expr.isLongConst && defaultValues.contains(expr.asLongConst.value) || + expr.isStringConst && defaultValues.contains(expr.asStringConst.value) || + expr.isNullExpr && defaultValues.contains(null) + } + } + + /** + * Checks whether the non-constant expression of the if-Statement is a read of the current + * field. + */ + def isGuardInternal( + expr: V, + tacCode: TACode[TACMethodParameter, V] + ): Boolean = { + expr.definedBy forall { index => + if (index < 0) + false // If the value is from a parameter, this can not be the guard + else { + val expression = code(index).asAssignment.expr + // in case of Integer etc.... .initValue() + if (expression.isVirtualFunctionCall) { + val virtualFunctionCall = expression.asVirtualFunctionCall + virtualFunctionCall.receiver.asVar.definedBy.forall(receiverDefSite => + receiverDefSite >= 0 && + isReadOfCurrentField(code(receiverDefSite).asAssignment.expr, tacCode, index) + ) + } else + isReadOfCurrentField(expression, tacCode, index) + } + } + } + + // Special handling for these types needed because of compare function in bytecode + def hasFloatDoubleOrLongType(fieldType: FieldType): Boolean = + fieldType.isFloatType || fieldType.isDoubleType || fieldType.isLongType + + if (ifStmt.rightExpr.isVar && hasFloatDoubleOrLongType(state.field.fieldType) && + ifStmt.rightExpr.asVar.definedBy.head > 0 && + tacCode.stmts(ifStmt.rightExpr.asVar.definedBy.head).asAssignment.expr.isCompare + ) { + + val left = + tacCode.stmts(ifStmt.rightExpr.asVar.definedBy.head).asAssignment.expr.asCompare.left.asVar + val right = + tacCode.stmts(ifStmt.rightExpr.asVar.definedBy.head).asAssignment.expr.asCompare.right.asVar + val leftExpr = tacCode.stmts(left.definedBy.head).asAssignment.expr + val rightExpr = tacCode.stmts(right.definedBy.head).asAssignment.expr + + if (leftExpr.isGetField || leftExpr.isGetStatic) isDefaultConst(rightExpr) + else (rightExpr.isGetField || rightExpr.isGetStatic) && isDefaultConst(leftExpr) + + } else if (ifStmt.leftExpr.isVar && ifStmt.rightExpr.isVar && ifStmt.leftExpr.asVar.definedBy.head >= 0 && + ifStmt.rightExpr.asVar.definedBy.head >= 0 && + hasFloatDoubleOrLongType(state.field.fieldType) && tacCode + .stmts(ifStmt.leftExpr.asVar.definedBy.head) + .asAssignment + .expr + .isCompare && + ifStmt.leftExpr.isVar && ifStmt.rightExpr.isVar + ) { + + val left = + tacCode.stmts(ifStmt.leftExpr.asVar.definedBy.head).asAssignment.expr.asCompare.left.asVar + val right = + tacCode.stmts(ifStmt.leftExpr.asVar.definedBy.head).asAssignment.expr.asCompare.right.asVar + val leftExpr = tacCode.stmts(left.definedBy.head).asAssignment.expr + val rightExpr = tacCode.stmts(right.definedBy.head).asAssignment.expr + + if (leftExpr.isGetField || leftExpr.isGetStatic) isDefaultConst(rightExpr) + else (rightExpr.isGetField || rightExpr.isGetStatic) && isDefaultConst(leftExpr) + + } else if (ifStmt.rightExpr.isVar && isDefaultConst(ifStmt.leftExpr)) { + isGuardInternal(ifStmt.rightExpr.asVar, tacCode) + } else if (ifStmt.leftExpr.isVar && isDefaultConst(ifStmt.rightExpr)) { + isGuardInternal(ifStmt.leftExpr.asVar, tacCode) + } else false + } + + /** + * Checks that the returned value is definitely read from the field. + */ + private def isFieldValueReturned( + write: FieldWriteAccessStmt[V], + writeIndex: Int, + readIndex: Int, + tac: TACode[TACMethodParameter, V], + guardIndexes: List[(Int, Int, Int, Int)] + )(implicit state: AnalysisState): Boolean = { + + def isSimpleReadOfField(expr: Expr[V]) = + expr.astID match { + + case GetField.ASTID => + val objRefDefinition = expr.asGetField.objRef.asVar.definedBy + if (objRefDefinition != SelfReferenceParameter) + false + else + expr.asGetField.resolveField(using project).contains(state.field) + + case GetStatic.ASTID => expr.asGetStatic.resolveField(using project).contains(state.field) + + case _ => false + } + + tac.stmts.forall { stmt => + !stmt.isReturnValue || { + + val returnValueDefs = stmt.asReturnValue.expr.asVar.definedBy + val assignedValueDefSite = write.value.asVar.definedBy + returnValueDefs.forall(_ >= 0) && { + if (returnValueDefs.size == 1 && returnValueDefs.head != readIndex) { + val expr = tac.stmts(returnValueDefs.head).asAssignment.expr + isSimpleReadOfField(expr) && guardIndexes.exists { + case (_, guardIndex, defaultCase, _) => + dominates(guardIndex, returnValueDefs.head, tac) && + (!dominates(defaultCase, returnValueDefs.head, tac) || + dominates(writeIndex, returnValueDefs.head, tac)) + } + } // The field is either read before the guard and returned or + // the value assigned to the field is returned + else { + returnValueDefs.size == 2 && assignedValueDefSite.size == 1 && + returnValueDefs.contains(readIndex) && { + returnValueDefs.contains(assignedValueDefSite.head) || { + val potentiallyReadIndex = returnValueDefs.filter(_ != readIndex).head + val expr = tac.stmts(potentiallyReadIndex).asAssignment.expr + isSimpleReadOfField(expr) && + guardIndexes.exists { + case (_, guardIndex, defaultCase, _) => + dominates(guardIndex, potentiallyReadIndex, tac) && + (!dominates(defaultCase, returnValueDefs.head, tac) || + dominates(writeIndex, returnValueDefs.head, tac)) + } + } + } + } + } + } + } + } +} From dfd76472d34295751342442c8f58b09f52c238b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 29 Oct 2025 20:07:31 +0100 Subject: [PATCH 02/41] Disable simple string model test for now --- .../openworld/stringelements/SimpleStringModel.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java index a50a78ade8..0ea12bfa6c 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java @@ -1,6 +1,7 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.fixtures.immutability.openworld.stringelements; +import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.UnsafelyLazilyInitializedField; import org.opalj.fpcf.properties.immutability.fields.NonTransitivelyImmutableField; import org.opalj.fpcf.properties.immutability.fields.TransitivelyImmutableField; @@ -17,25 +18,27 @@ public final class SimpleStringModel { @TransitivelyImmutableField(value = "The array values are not mutated after the assignment ", analyses = {}) @NonTransitivelyImmutableField(value = "The analysis can not recognize transitive immutable arrays", - analyses = { FieldImmutabilityAnalysis.class}) - @NonAssignableField("The field is final") + analyses = { FieldImmutabilityAnalysis.class }) + @NonAssignableField(value = "The field is final", analyses = {}) + @AssignableField(value = "The field is written read and written in two different initializers", + analyses = { L2FieldAssignabilityAnalysis.class }) private final char value[]; - public char[] getValue(){ + public char[] getValue() { return value.clone(); } @TransitivelyImmutableField(value = "Lazy initialized field with primitive type", analyses = {}) @LazilyInitializedField(value = "Field is lazily initialized", analyses = {}) @UnsafelyLazilyInitializedField(value = "The analysis cannot recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int hash; // Default value 0 public SimpleStringModel(SimpleStringModel original) { this.value = original.value; } - public SimpleStringModel(char[] value){ + public SimpleStringModel(char[] value) { this.value = value.clone(); } From 7024efe954acdc89e4b33aded24208b3180603ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 29 Oct 2025 20:26:40 +0100 Subject: [PATCH 03/41] Correctly refresh assignability on new TAC --- .../AbstractFieldAssignabilityAnalysis.scala | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index 5464621a62..cb73f608ba 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -71,12 +71,6 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { var tacDependees: Map[DefinedMethod, EOptionP[Method, TACAI]] = Map.empty - def forEachAccessInMethod(accesses: Map[Context, Set[(PC, AccessReceiver)]], definedMethod: DefinedMethod)( - f: (Context, Set[(PC, AccessReceiver)]) => Unit - ): Unit = { - accesses.iterator.filter(_._1.method eq definedMethod).foreach(kv => f(kv._1, kv._2)) - } - def hasDependees: Boolean = { fieldWriteAccessDependee.exists(_.isRefinable) || fieldReadAccessDependee.exists(_.isRefinable) || @@ -179,18 +173,33 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { case TACAI.key => val newEP = eps.asInstanceOf[EOptionP[Method, TACAI]] val method = declaredMethods(newEP.e) + if (state.tacDependees(method).hasUBP) + throw IllegalStateException("True updates to the TAC (UB -> new UB) are not supported yet!") state.tacDependees += method -> newEP val tac = newEP.ub.tac.get - // Renew field assignability analysis for all field accesses - state.forEachAccessInMethod(state.initializerWrites, method) { (context, accesses) => // TODO do for each access type - accesses.foreach { case (pc, receiver) => - val receiverVar = receiver.map(uVarForDefSites(_, tac.pcToIndex)) - if (state.fieldAssignability != Assignable) - state.fieldAssignability = state.fieldAssignability.meet { - analyzeInitializerWrite(context, tac, pc, receiverVar) + + def refreshAssignability( + accesses: Map[Context, Set[(PC, AccessReceiver)]], + analyzeFunc: (Context, TACode[TACMethodParameter, V], PC, Option[V]) => FieldAssignability + ): Unit = { + if (state.fieldAssignability != Assignable) { + accesses.iterator.filter(_._1.method eq method).foreach { case (context, accessesInContext) => + accessesInContext.foreach { case (pc, receiver) => + val receiverVar = receiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (state.fieldAssignability != Assignable) + state.fieldAssignability = state.fieldAssignability.meet { + analyzeFunc(context, tac, pc, receiverVar) + } } + } } } + + refreshAssignability(state.initializerReads, analyzeInitializerRead) + refreshAssignability(state.nonInitializerReads, analyzeNonInitializerRead) + refreshAssignability(state.initializerWrites, analyzeInitializerWrite) + refreshAssignability(state.nonInitializerWrites, analyzeNonInitializerWrite) + createResult() } } From 09d026e76c1a3f285aa77de71f10c25ac4e589de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 29 Oct 2025 20:44:23 +0100 Subject: [PATCH 04/41] Perform soundness check --- .../fieldassignability/L2FieldAssignabilityAnalysis.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index ed06e17198..7b10940905 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -225,7 +225,8 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som // ... we identify easy instances of field reads (interaction with potentially disruptive reads // is checked separately; note that this inference relies on a flat TAC) OR ... stmt.isAssignment && stmt.asAssignment.expr.isGetField || - // ... we easily identify the use site to initialize an object OR ... TODO check that this is sound + // ... we easily identify the use site to initialize an object (fine as initializer reads outside the + // current method are forbidden) OR ... stmt.isMethodCall && stmt.asMethodCall.name == "" || // ... the statement is irrelevant, since there is no instance where the write did not take effect yet !pathExists(stmt.pc, writePC, tac) From 0890e33bb6d31e63d4ea48a884f53e3bae3c7e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 29 Oct 2025 21:04:54 +0100 Subject: [PATCH 05/41] Fix call to lazy initializer pattern --- .../fieldassignability/L2FieldAssignabilityAnalysis.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 7b10940905..79c1ce1a00 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -100,7 +100,7 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som writePC: PC, receiver: Option[V] )(implicit state: State): FieldAssignability = { - val assignability = completeLazyInitPatternForInitializerRead() + val assignability = completeLazyInitPatternForInitializerWrite() if (assignability == Assignable) return Assignable; From 30b8e01af9543cf1076678cf32e49f6635498d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 29 Oct 2025 21:07:07 +0100 Subject: [PATCH 06/41] Enable more lazy initialization tests --- .../LazyInitializationPrimitiveTypes.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java index c32fd15f7d..abdee9995b 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java @@ -295,7 +295,9 @@ public int init() { class CaughtExceptionInInitialization { - //TODO @LazilyInitializedField("Despite the possible exception the field is always seen with one value") + @LazilyInitializedField(value = "The field is always seen with one value", analyses = {}) + @AssignableField(value = "The possible exception prevents the analysis", + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init(int i) { @@ -311,3 +313,17 @@ public int init(int i) { } } } + +class PreInitializedDefaultValueField { + + @UnsafelyLazilyInitializedField(value = "The field is always seen with one value") + private Object lazyInitField = null; + + public Object getLazyInitField() { + if (this.lazyInitField == null) { + this.lazyInitField = new Object(); + } + + return this.lazyInitField; + } +} From b7ca155cec3809abd416d9fff4da0110d452cac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 16:14:49 +0100 Subject: [PATCH 07/41] Disable failing test --- .../primitive_types/LazyInitializationPrimitiveTypes.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java index abdee9995b..9a5e9a470a 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java @@ -316,7 +316,9 @@ public int init(int i) { class PreInitializedDefaultValueField { - @UnsafelyLazilyInitializedField(value = "The field is always seen with one value") + @UnsafelyLazilyInitializedField(value = "The field is always seen with one value", analyses = {}) + @AssignableField(value = "The analysis cannot currently handle initial default value assignments", + analyses = { L2FieldAssignabilityAnalysis.class }) private Object lazyInitField = null; public Object getLazyInitField() { From de441377dbf30eb7f37c77e6885a4b0ee73d463f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 16:19:39 +0100 Subject: [PATCH 08/41] Slightly more efficient lazy init check --- .../LazyInitializationAnalysis.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala index a98d2e0a79..2d66ad5a17 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala @@ -145,7 +145,7 @@ trait LazyInitializationAnalysis private[fieldassignability] if (state.initializerWrites.nonEmpty) return Assignable; - // E.g. multiple lazy initialization patterns cannot be supported in a collaborative setting + // Multiple lazy initialization patterns cannot be supported in a collaborative setting if (state.nonInitializerWrites.iterator.distinctBy(_._1.method).size > 1) return Assignable; @@ -153,6 +153,11 @@ trait LazyInitializationAnalysis private[fieldassignability] if (state.nonInitializerWrites(context).size > 1) return Assignable; + // A lazy init does not allow reads outside the lazy initialization method, effectively also preventing analysis + // of patterns with multiple lazy-init functions. + if (state.initializerReads.nonEmpty || state.nonInitializerReads.exists(_._1.method ne context.method)) + return Assignable; + val method = context.method.definedMethod val writeIndex = tac.pcToIndex(writePC) val cfg = tac.cfg @@ -205,11 +210,6 @@ trait LazyInitializationAnalysis private[fieldassignability] return Assignable; } - // A lazy init does not allow considering reads outside the lazy initialization method, effectively also - // preventing analysis multi-lazy-init patterns. - if (state.initializerReads.nonEmpty || state.nonInitializerReads.exists(_._1.method ne context.method)) - return Assignable; - if (doFieldReadsEscape(state.nonInitializerReads(context).map(_._1), guardIndex, writeIndex, tac)) return Assignable; From 6f4f3033ac6721cb9781d72f52e9a42492f2b43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 20:39:17 +0100 Subject: [PATCH 09/41] Extract lazy init and clone pattern functions --- .../AbstractFieldAssignabilityAnalysis.scala | 85 +++++++------ .../ClonePatternAnalysis.scala | 104 ++++++++++++++++ .../FieldAssignabilityAnalysisPart.scala | 37 ++++++ .../L0FieldAssignabilityAnalysis.scala | 2 + .../L2FieldAssignabilityAnalysis.scala | 77 ++++++------ .../LazyInitializationAnalysis.scala | 116 ++++++++++-------- 6 files changed, 286 insertions(+), 135 deletions(-) create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index cb73f608ba..9bccec5910 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -46,6 +46,34 @@ import org.opalj.tac.common.DefinitionSitesKey import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites import org.opalj.tac.fpcf.properties.TACAI +trait AbstractFieldAssignabilityAnalysisState { + + val field: Field + var fieldAssignability: FieldAssignability = NonAssignable + + var fieldWriteAccessDependee: Option[EOptionP[DeclaredField, FieldWriteAccessInformation]] = None + var initializerWrites: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + var nonInitializerWrites: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + + var fieldReadAccessDependee: Option[EOptionP[DeclaredField, FieldReadAccessInformation]] = None + var initializerReads: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + var nonInitializerReads: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) + + var tacDependees: Map[DefinedMethod, EOptionP[Method, TACAI]] = Map.empty + + def hasDependees: Boolean = { + fieldWriteAccessDependee.exists(_.isRefinable) || + fieldReadAccessDependee.exists(_.isRefinable) || + tacDependees.valuesIterator.exists(_.isRefinable) + } + + def dependees: Set[SomeEOptionP] = { + tacDependees.valuesIterator.filter(_.isRefinable).toSet ++ + fieldWriteAccessDependee.filter(_.isRefinable) ++ + fieldReadAccessDependee.filter(_.isRefinable) + } +} + /** * TODO document * @@ -56,32 +84,25 @@ import org.opalj.tac.fpcf.properties.TACAI */ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { - trait AbstractFieldAssignabilityAnalysisState { - - val field: Field - var fieldAssignability: FieldAssignability = NonAssignable - - var fieldWriteAccessDependee: Option[EOptionP[DeclaredField, FieldWriteAccessInformation]] = None - var initializerWrites: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) - var nonInitializerWrites: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) - - var fieldReadAccessDependee: Option[EOptionP[DeclaredField, FieldReadAccessInformation]] = None - var initializerReads: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) - var nonInitializerReads: Map[Context, Set[(PC, AccessReceiver)]] = Map.empty.withDefaultValue(Set.empty) - - var tacDependees: Map[DefinedMethod, EOptionP[Method, TACAI]] = Map.empty - - def hasDependees: Boolean = { - fieldWriteAccessDependee.exists(_.isRefinable) || - fieldReadAccessDependee.exists(_.isRefinable) || - tacDependees.valuesIterator.exists(_.isRefinable) + protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] + + protected def determineAssignabilityFromParts( + partFunc: FieldAssignabilityAnalysisPart => Option[FieldAssignability] + ): Option[FieldAssignability] = { + var assignability: Option[FieldAssignability] = None + for { + part <- parts + if !assignability.contains(Assignable) + partAssignability = partFunc(part) + if partAssignability.isDefined + } { + if (assignability.isDefined) + assignability = Some(assignability.get.meet(partAssignability.get)) + else + assignability = Some(partAssignability.get) } - def dependees: Set[SomeEOptionP] = { - tacDependees.valuesIterator.filter(_.isRefinable).toSet ++ - fieldWriteAccessDependee.filter(_.isRefinable) ++ - fieldReadAccessDependee.filter(_.isRefinable) - } + assignability } final val typeExtensibility = project.get(TypeExtensibilityKey) @@ -319,22 +340,6 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { createResult() } - /** - * Determines whether the basic block of a given index dominates the basic block of the other index or is executed - * before the other index in the case of both indexes belonging to the same basic block. - */ - protected def dominates( - potentiallyDominatorIndex: Int, - potentiallyDominatedIndex: Int, - taCode: TACode[TACMethodParameter, V] - ): Boolean = { - val bbPotentiallyDominator = taCode.cfg.bb(potentiallyDominatorIndex) - val bbPotentiallyDominated = taCode.cfg.bb(potentiallyDominatedIndex) - taCode.cfg.dominatorTree - .strictlyDominates(bbPotentiallyDominator.nodeId, bbPotentiallyDominated.nodeId) || - bbPotentiallyDominator == bbPotentiallyDominated && potentiallyDominatorIndex < potentiallyDominatedIndex - } - /** * Provided with two PCs, determines whether there exists a path from the first PC to the second PC in the context * of the provided TAC and attached CFG. This check is not reflexive. diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala new file mode 100644 index 0000000000..9bb186ba3f --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -0,0 +1,104 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability + +import org.opalj.br.PC +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.immutability.Assignable +import org.opalj.br.fpcf.properties.immutability.FieldAssignability +import org.opalj.br.fpcf.properties.immutability.NonAssignable +import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites + +/** + * Determines whether a field write access corresponds to a lazy initialization of the field. + * + * @note Requires that the 3-address code's expressions are not deeply nested. + * + * @author Maximilian Rüsch + */ +trait ClonePatternAnalysis private[fieldassignability] + extends FieldAssignabilityAnalysisPart { + + val considerLazyInitialization: Boolean = + project.config.getBoolean( + "org.opalj.fpcf.analyses.L2FieldAssignabilityAnalysis.considerLazyInitialization" + ) + + /** + * @note To be provided by the part user. + */ + protected val isSameInstance: (tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V) => Answer + + /** + * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not + * make the field assignable in combination with the given write itself. + * + * @note To be provided by the part user. + */ + protected val isWrittenInstanceUseSiteSafe: (tac: TACode[TACMethodParameter, V], writePC: Int, use: Int) => Boolean + + /** + * Provided with two PCs, determines irreflexively whether there exists a path from the first PC to the second PC in + * the context of the provided TAC and attached CFG. + * + * @note To be provided by the part user. + */ + protected val pathExists: (fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]) => Boolean + + override def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] = None + + override def completePatternWithNonInitializerRead( + context: Context, + readPC: Int + )(implicit state: AnalysisState): Option[FieldAssignability] = None + + override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = None + + override def completePatternWithNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = { + if (state.field.isStatic) + return None; + + if (receiver.isEmpty) + return Some(Assignable); + + if (receiver.get.definedBy.size > 1) + return Some(Assignable); + + if (receiver.get.definedBy.head <= OriginOfThis) + return None; + + // Check for a clone pattern: Must be on a fresh instance (i.e. cannot be "this" or a parameter) ... + val defSite = receiver.get.definedBy.head + if (!tac.stmts(defSite).asAssignment.expr.isNew) + return Some(Assignable); + + // ... free of dangerous uses, ... + val useSites = tac.stmts(defSite).asAssignment.targetVar.usedBy + if (!useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC, _))) + return Some(Assignable); + + // ... and not contain read-write paths, whenever their receivers cannot be proven to be disjoint + val pathFromSomeReadToWriteExists = state.nonInitializerReads(context).exists { + case (readPC, readReceiver) => + val readReceiverVar = readReceiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (readReceiverVar.isDefined && isSameInstance(tac, receiver.get, readReceiverVar.get).isNo) { + false + } else { + pathExists(readPC, writePC, tac) + } + } + + if (pathFromSomeReadToWriteExists) + Some(Assignable) + else + Some(NonAssignable) + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala new file mode 100644 index 0000000000..71361c57ad --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala @@ -0,0 +1,37 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability + +import org.opalj.br.PC +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.immutability.FieldAssignability + + +/** + * @author Maximilian Rüsch + */ +trait FieldAssignabilityAnalysisPart private[fieldassignability] + extends FPCFAnalysis { + + type AnalysisState <: AbstractFieldAssignabilityAnalysisState + + def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] + + def completePatternWithNonInitializerRead( + context: Context, + readPC: Int + )(implicit state: AnalysisState): Option[FieldAssignability] + + def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] + + def completePatternWithNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index dd7c9c01ab..ccf4f15dc7 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -29,6 +29,8 @@ class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: Som type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) + override protected lazy val parts = Seq.empty[FieldAssignabilityAnalysisPart] + override def analyzeInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 79c1ce1a00..4e3fcf996b 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -30,7 +30,6 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites */ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - with LazyInitializationAnalysis with FPCFAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState @@ -38,14 +37,32 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) + private class L2LazyInitializationPart(val project: SomeProject) extends LazyInitializationAnalysis { + override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState + } + private class L2ClonePatternPart(val project: SomeProject) extends ClonePatternAnalysis { + override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState + override val isSameInstance = L2FieldAssignabilityAnalysis.this.isSameInstance + override val isWrittenInstanceUseSiteSafe = L2FieldAssignabilityAnalysis.this + .isWrittenInstanceUseSiteSafe + override val pathExists = L2FieldAssignabilityAnalysis.this.pathExists + } + + override protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] = List( + new L2LazyInitializationPart(project), + new L2ClonePatternPart(project) + ) + override def analyzeInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: PC, receiver: Option[V] )(implicit state: State): FieldAssignability = { - val assignability = completeLazyInitPatternForInitializerRead() - if (assignability == Assignable) + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithInitializerRead()(using state.asInstanceOf[part.AnalysisState]) + ) + if (assignability.contains(Assignable)) return Assignable; // Initializer reads are incompatible with arbitrary writes in other methods @@ -73,8 +90,10 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som readPC: PC, receiver: Option[V] )(implicit state: State): FieldAssignability = { - val assignability = completeLazyInitPatternForNonInitializerRead(context, readPC) - if (assignability == Assignable) + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithNonInitializerRead(context, readPC)(using state.asInstanceOf[part.AnalysisState]) + ) + if (assignability.contains(Assignable)) return Assignable; // Completes the analysis of the clone pattern by recognizing unsafe read-write paths on the same field @@ -100,8 +119,10 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som writePC: PC, receiver: Option[V] )(implicit state: State): FieldAssignability = { - val assignability = completeLazyInitPatternForInitializerWrite() - if (assignability == Assignable) + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithInitializerWrite()(using state.asInstanceOf[part.AnalysisState]) + ) + if (assignability.contains(Assignable)) return Assignable; val method = context.method.definedMethod @@ -115,7 +136,7 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som if (receiver.isEmpty || receiver.get.definedBy != SelfReferenceParameter) return Assignable; - if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC))) + if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC, _))) return Assignable; } @@ -145,38 +166,12 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som writePC: PC, receiver: Option[V] )(implicit state: State): FieldAssignability = { - if (state.field.isStatic || receiver.isDefined && receiver.get.definedBy == SelfReferenceParameter) { - completeLazyInitPatternForNonInitializerWrite(context, tac, writePC) - } else if (receiver.isDefined) { - // Check for a clone pattern: Must be on a fresh instance (i.e. cannot be "this" or a parameter) ... - if (receiver.get.definedBy.exists(_ <= OriginOfThis) || receiver.get.definedBy.size > 1) - return Assignable; - val defSite = receiver.get.definedBy.head - if (!tac.stmts(defSite).asAssignment.expr.isNew) - return Assignable; - - // ... free of dangerous uses, ... - val useSites = tac.stmts(defSite).asAssignment.targetVar.usedBy - if (!useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC))) - return Assignable; - - // ... and not contain read-write paths, whenever their receivers cannot be proven to be disjoint - val pathFromSomeReadToWriteExists = state.nonInitializerReads(context).exists { - case (readPC, readReceiver) => - val readReceiverVar = readReceiver.map(uVarForDefSites(_, tac.pcToIndex)) - if (readReceiverVar.isDefined && isSameInstance(tac, receiver.get, readReceiverVar.get).isNo) { - false - } else { - pathExists(readPC, writePC, tac) - } - } - - if (pathFromSomeReadToWriteExists) - Assignable - else - NonAssignable - } else - Assignable + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithNonInitializerWrite(context, tac, writePC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) + ) + assignability.getOrElse(Assignable) } /** @@ -214,7 +209,7 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not * make the field assignable in combination with the given write itself. */ - private def isWrittenInstanceUseSiteSafe(tac: TACode[TACMethodParameter, V], writePC: Int)(use: Int): Boolean = { + private def isWrittenInstanceUseSiteSafe(tac: TACode[TACMethodParameter, V], writePC: Int, use: Int): Boolean = { val stmt = tac.stmts(use) // We consider the access safe when ... diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala index 2d66ad5a17..da7ad2c6bf 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala @@ -26,15 +26,12 @@ import org.opalj.br.PC import org.opalj.br.PCs import org.opalj.br.ReferenceType import org.opalj.br.ShortType -import org.opalj.br.analyses.SomeProject import org.opalj.br.cfg.BasicBlock import org.opalj.br.cfg.CFGNode -import org.opalj.br.fpcf.FPCFAnalysis import org.opalj.br.fpcf.properties.Context import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.LazilyInitialized -import org.opalj.br.fpcf.properties.immutability.NonAssignable import org.opalj.br.fpcf.properties.immutability.UnsafelyLazilyInitialized import org.opalj.collection.immutable.IntTrieSet import org.opalj.tac.CaughtException @@ -55,6 +52,10 @@ import org.opalj.tac.TACode import org.opalj.tac.Throw import org.opalj.tac.VirtualFunctionCall +trait LazyInitializationAnalysisState extends AbstractFieldAssignabilityAnalysisState { + var potentialLazyInit: Option[(Context, Int, Int, TACode[TACMethodParameter, V])] = None +} + /** * Determines whether a field write access corresponds to a lazy initialization of the field. * @@ -67,17 +68,10 @@ import org.opalj.tac.VirtualFunctionCall * @author Maximilian Rüsch */ trait LazyInitializationAnalysis private[fieldassignability] - extends AbstractFieldAssignabilityAnalysis - with FPCFAnalysis { - - trait LazyInitializationAnalysisState extends AbstractFieldAssignabilityAnalysisState { - // context : index of guard statement : index of write statement : TAC - var potentialLazyInit: Option[(Context, Int, Int, TACode[TACMethodParameter, V])] = None - } + extends FieldAssignabilityAnalysisPart { override type AnalysisState <: LazyInitializationAnalysisState - val project: SomeProject val considerLazyInitialization: Boolean = project.config.getBoolean( "org.opalj.fpcf.analyses.L2FieldAssignabilityAnalysis.considerLazyInitialization" @@ -97,66 +91,80 @@ trait LazyInitializationAnalysis private[fieldassignability] case _: ReferenceType => Set(null) } - def completeLazyInitPatternForInitializerRead()(implicit state: AnalysisState): FieldAssignability = { - if (state.potentialLazyInit.isEmpty) - NonAssignable - else - Assignable + /** + * Determines whether the basic block of a given index dominates the basic block of the other index or is executed + * before the other index in the case of both indexes belonging to the same basic block. + */ + private def dominates( + potentiallyDominatorIndex: Int, + potentiallyDominatedIndex: Int, + taCode: TACode[TACMethodParameter, V] + ): Boolean = { + val bbPotentiallyDominator = taCode.cfg.bb(potentiallyDominatorIndex) + val bbPotentiallyDominated = taCode.cfg.bb(potentiallyDominatedIndex) + taCode.cfg.dominatorTree + .strictlyDominates(bbPotentiallyDominator.nodeId, bbPotentiallyDominated.nodeId) || + bbPotentiallyDominator == bbPotentiallyDominated && potentiallyDominatorIndex < potentiallyDominatedIndex } - def completeLazyInitPatternForNonInitializerRead( + override def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] = + state.potentialLazyInit.map(_ => Assignable) + + override def completePatternWithNonInitializerRead( context: Context, readPC: Int - )(implicit state: AnalysisState): FieldAssignability = { + )(implicit state: AnalysisState): Option[FieldAssignability] = { // No lazy init pattern exists, or it was not discovered yet if (state.potentialLazyInit.isEmpty) - return NonAssignable; + return None; val (lazyInitContext, guardIndex, writeIndex, tac) = state.potentialLazyInit.get // We only support lazy initialization patterns fully contained within one method, but different contexts of the // same method are fine. if (context.method ne lazyInitContext.method) - return Assignable; + return Some(Assignable); if (context.id != lazyInitContext.id) - return NonAssignable; + return None; if (doFieldReadsEscape(Set(readPC), guardIndex, writeIndex, tac)) - Assignable - else - NonAssignable - } + return Some(Assignable); - def completeLazyInitPatternForInitializerWrite()(implicit state: AnalysisState): FieldAssignability = { - if (state.potentialLazyInit.isEmpty) - NonAssignable - else - Assignable + None } - /** - * To be called when a non-initializer write on either a static field or a 'this' reference is discovered. - */ - def completeLazyInitPatternForNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC - )(implicit state: AnalysisState): FieldAssignability = { + override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = + state.potentialLazyInit.map(_ => Assignable) + + override def completePatternWithNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = { + if (state.field.isNotStatic) { + if (receiver.isEmpty) + return Some(Assignable); + + if (receiver.get.definedBy != SelfReferenceParameter) + return None; + } + // A lazy initialization pattern does not allow initializing a field regularly if (state.initializerWrites.nonEmpty) - return Assignable; + return Some(Assignable); // Multiple lazy initialization patterns cannot be supported in a collaborative setting if (state.nonInitializerWrites.iterator.distinctBy(_._1.method).size > 1) - return Assignable; + return Some(Assignable); // We do not support multiple-write lazy initializations yet if (state.nonInitializerWrites(context).size > 1) - return Assignable; + return Some(Assignable); // A lazy init does not allow reads outside the lazy initialization method, effectively also preventing analysis // of patterns with multiple lazy-init functions. if (state.initializerReads.nonEmpty || state.nonInitializerReads.exists(_._1.method ne context.method)) - return Assignable; + return Some(Assignable); val method = context.method.definedMethod val writeIndex = tac.pcToIndex(writePC) @@ -165,31 +173,31 @@ trait LazyInitializationAnalysis private[fieldassignability] // We only support lazy initialization using direct field writes if (!tac.stmts(writeIndex).isFieldWriteAccessStmt) - return Assignable; + return Some(Assignable); val writeStmt = tac.stmts(writeIndex).asFieldWriteAccessStmt val resultCatchesAndThrows = findCatchesAndThrows(tac) val findGuardsResult = findGuards(writeIndex, tac) // no guard -> no lazy initialization if (findGuardsResult.isEmpty) - return Assignable; + return Some(Assignable); val (readIndex, guardIndex, defaultCaseIndex, elseCaseIndex) = findGuardsResult.head // The field has to be written when the guard is in the default-case branch if (!dominates(defaultCaseIndex, writeIndex, tac)) - return Assignable; + return Some(Assignable); val elseBB = cfg.bb(elseCaseIndex) // prevents wrong control flow if (isTransitivePredecessor(elseBB, writeBB)) - return Assignable; + return Some(Assignable); if (method.returnType == state.field.fieldType) { // prevents that another value than the field value is returned with the same type if (!isFieldValueReturned(writeStmt, writeIndex, readIndex, tac, findGuardsResult)) - return Assignable; + return Some(Assignable); // prevents that the field is seen with another value if ( // potentially unsound with method.returnType == state.field.fieldType // TODO comment it out and look at appearing cases @@ -207,13 +215,13 @@ trait LazyInitializationAnalysis private[fieldassignability] } ) ) - return Assignable; + return Some(Assignable); } if (doFieldReadsEscape(state.nonInitializerReads(context).map(_._1), guardIndex, writeIndex, tac)) - return Assignable; + return Some(Assignable); - state.potentialLazyInit = Some((context, guardIndex, writeIndex, tac)) + state.potentialLazyInit = Some(context, guardIndex, writeIndex, tac) /** * Determines whether all caught exceptions are thrown afterwards @@ -233,17 +241,17 @@ trait LazyInitializationAnalysis private[fieldassignability] noInterferingExceptions() ) { if (method.isSynchronized) - LazilyInitialized + Some(LazilyInitialized) else { val (indexMonitorEnter, indexMonitorExit) = findMonitors(writeIndex, tac) val monitorResultsDefined = indexMonitorEnter.isDefined && indexMonitorExit.isDefined if (monitorResultsDefined && dominates(indexMonitorEnter.get, readIndex, tac)) - LazilyInitialized + Some(LazilyInitialized) else - UnsafelyLazilyInitialized + Some(UnsafelyLazilyInitialized) } } else - Assignable + Some(Assignable) } private def doFieldReadsEscape( From 676989d92476957e68f3e1a70b1602e97035b59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 20:51:47 +0100 Subject: [PATCH 10/41] Make analysis of non-initializer reads in clone pattern symmetric inside handler --- .../ClonePatternAnalysis.scala | 21 +++++++++++++++-- .../FieldAssignabilityAnalysisPart.scala | 13 ++++++----- .../L2FieldAssignabilityAnalysis.scala | 23 ++++--------------- .../LazyInitializationAnalysis.scala | 6 +++-- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala index 9bb186ba3f..50c9bc7543 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -52,8 +52,25 @@ trait ClonePatternAnalysis private[fieldassignability] override def completePatternWithNonInitializerRead( context: Context, - readPC: Int - )(implicit state: AnalysisState): Option[FieldAssignability] = None + tac: TACode[TACMethodParameter, V], + readPC: Int, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = { + val pathFromReadToSomeWriteExists = state.nonInitializerWrites(context).exists { + case (writePC, writeReceiver) => + val writeReceiverVar = writeReceiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (writeReceiverVar.isDefined && isSameInstance(tac, receiver.get, writeReceiverVar.get).isNo) { + false + } else { + pathExists(readPC, writePC, tac) + } + } + + if (pathFromReadToSomeWriteExists) + Some(Assignable) + else + None + } override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = None diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala index 71361c57ad..d5dd6bd492 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala @@ -10,7 +10,6 @@ import org.opalj.br.fpcf.FPCFAnalysis import org.opalj.br.fpcf.properties.Context import org.opalj.br.fpcf.properties.immutability.FieldAssignability - /** * @author Maximilian Rüsch */ @@ -22,16 +21,18 @@ trait FieldAssignabilityAnalysisPart private[fieldassignability] def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] def completePatternWithNonInitializerRead( - context: Context, - readPC: Int + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: Int, + receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] def completePatternWithNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 4e3fcf996b..697d9251af 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -91,26 +91,11 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som receiver: Option[V] )(implicit state: State): FieldAssignability = { val assignability = determineAssignabilityFromParts(part => - part.completePatternWithNonInitializerRead(context, readPC)(using state.asInstanceOf[part.AnalysisState]) + part.completePatternWithNonInitializerRead(context, tac, readPC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) ) - if (assignability.contains(Assignable)) - return Assignable; - - // Completes the analysis of the clone pattern by recognizing unsafe read-write paths on the same field - val pathFromReadToSomeWriteExists = state.nonInitializerWrites(context).exists { - case (writePC, writeReceiver) => - val writeReceiverVar = writeReceiver.map(uVarForDefSites(_, tac.pcToIndex)) - if (writeReceiverVar.isDefined && isSameInstance(tac, receiver.get, writeReceiverVar.get).isNo) { - false - } else { - pathExists(readPC, writePC, tac) - } - } - - if (pathFromReadToSomeWriteExists) - Assignable - else - NonAssignable + assignability.getOrElse(NonAssignable) } override def analyzeInitializerWrite( diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala index da7ad2c6bf..a9148443e1 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala @@ -111,8 +111,10 @@ trait LazyInitializationAnalysis private[fieldassignability] state.potentialLazyInit.map(_ => Assignable) override def completePatternWithNonInitializerRead( - context: Context, - readPC: Int + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: Int, + receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] = { // No lazy init pattern exists, or it was not discovered yet if (state.potentialLazyInit.isEmpty) From f75f910a1f6e320e57264fde2b33cc51823ea90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 21:04:01 +0100 Subject: [PATCH 11/41] Allow initializer writes for lazy init pattern --- .../primitive_types/LazyInitializationPrimitiveTypes.java | 4 +--- .../fieldassignability/LazyInitializationAnalysis.scala | 7 +------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java index 9a5e9a470a..abdee9995b 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/primitive_types/LazyInitializationPrimitiveTypes.java @@ -316,9 +316,7 @@ public int init(int i) { class PreInitializedDefaultValueField { - @UnsafelyLazilyInitializedField(value = "The field is always seen with one value", analyses = {}) - @AssignableField(value = "The analysis cannot currently handle initial default value assignments", - analyses = { L2FieldAssignabilityAnalysis.class }) + @UnsafelyLazilyInitializedField(value = "The field is always seen with one value") private Object lazyInitField = null; public Object getLazyInitField() { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala index a9148443e1..ed488a5b7b 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala @@ -134,8 +134,7 @@ trait LazyInitializationAnalysis private[fieldassignability] None } - override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = - state.potentialLazyInit.map(_ => Assignable) + override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = None override def completePatternWithNonInitializerWrite( context: Context, @@ -151,10 +150,6 @@ trait LazyInitializationAnalysis private[fieldassignability] return None; } - // A lazy initialization pattern does not allow initializing a field regularly - if (state.initializerWrites.nonEmpty) - return Some(Assignable); - // Multiple lazy initialization patterns cannot be supported in a collaborative setting if (state.nonInitializerWrites.iterator.distinctBy(_._1.method).size > 1) return Some(Assignable); From ec3d93a31c1cd76777d7359bad89a59e4020fa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 22:07:08 +0100 Subject: [PATCH 12/41] Remove L1 from reference conf for now --- OPAL/tac/src/main/resources/reference.conf | 5 ----- 1 file changed, 5 deletions(-) diff --git a/OPAL/tac/src/main/resources/reference.conf b/OPAL/tac/src/main/resources/reference.conf index d02069a03d..af6259fb7c 100644 --- a/OPAL/tac/src/main/resources/reference.conf +++ b/OPAL/tac/src/main/resources/reference.conf @@ -17,11 +17,6 @@ org.opalj { eagerFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.EagerL0FieldAssignabilityAnalysis", lazyFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.LazyL0FieldAssignabilityAnalysis" }, - "L1FieldAssignabilityAnalysis" { - description = "Determines the assignability of (instance and static) fields.", - eagerFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.EagerL1FieldAssignabilityAnalysis", - lazyFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis" - }, "L2FieldAssignabilityAnalysis" { description = "Determines the assignability of (instance and static) fields.", eagerFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.EagerL2FieldAssignabilityAnalysis", From 18d664bee6a81d0ffd1d2a0a66657a6175703d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 12 Nov 2025 22:52:36 +0100 Subject: [PATCH 13/41] Fix support for reading static fields in constructors --- .../general/ClassWithPublicFinalFields.java | 20 +++++++++++++++ .../L2FieldAssignabilityAnalysis.scala | 25 +++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java new file mode 100644 index 0000000000..4fd54b6252 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java @@ -0,0 +1,20 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.general; + +import org.opalj.fpcf.properties.immutability.classes.MutableClass; +import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; +import org.opalj.fpcf.properties.immutability.fields.MutableField; +import org.opalj.fpcf.properties.immutability.types.MutableType; + +@MutableType("The class has mutable fields") +@MutableClass("The class has mutable fields") +public class ClassWithPublicFinalFields { + + @MutableField("Field is assignable") + @NonAssignableField("The field is public, final, and its value is only set once since it is static") + public static final Object DEFAULT = new Object(); + + public ClassWithPublicFinalFields() { + System.out.println(ClassWithPublicFinalFields.DEFAULT); + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 697d9251af..f8c28e206c 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -69,9 +69,14 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som if (state.nonInitializerWrites.nonEmpty) return Assignable; - // Analyzing read-write paths interprocedurally is not supported yet - if (state.initializerWrites.exists(_._1.method ne context.method)) - return Assignable; + // Analyzing read-write paths interprocedurally is not supported yet. For static fields, initializer writes are + // harmless (i.e. executed before) when the read is in a constructor, so the class is already initialized. + if (state.initializerWrites.exists(_._1.method ne context.method)) { + if (state.field.isNotStatic) + return Assignable; + else if (!context.method.definedMethod.isConstructor) + return Assignable; + } val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { case (writePC, _) => @@ -125,8 +130,18 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som return Assignable; } - // Analyzing read-write paths interprocedurally is not supported yet - if (state.initializerReads.exists(_._1.method ne context.method)) + // Analyzing read-write paths interprocedurally is not supported yet. However, for static fields, all + // reads that take place in instance constructors are harmless. + if (state.field.isNotStatic && state.initializerReads.exists(_._1.method ne context.method)) + return Assignable; + if (state.field.isStatic && state.initializerReads.exists { + case (readContext, _) => + (readContext.method ne context.method) && ( + !readContext.method.hasSingleDefinedMethod || + readContext.method.definedMethod.isStaticInitializer + ) + } + ) return Assignable; val pathFromSomeReadToWriteExists = state.initializerReads(context).exists { From 4adf80c87d19885adca5d124f55c9c87f6a538b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 00:14:58 +0100 Subject: [PATCH 14/41] Fix formatting --- .../analyses/fieldassignability/ClonePatternAnalysis.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala index 50c9bc7543..41eab293ab 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -51,9 +51,9 @@ trait ClonePatternAnalysis private[fieldassignability] override def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] = None override def completePatternWithNonInitializerRead( - context: Context, + context: Context, tac: TACode[TACMethodParameter, V], - readPC: Int, + readPC: Int, receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] = { val pathFromReadToSomeWriteExists = state.nonInitializerWrites(context).exists { From 45ebe3ed14848101b5298c9c17728d000ce7a2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 17:39:55 +0100 Subject: [PATCH 15/41] Fix immutability values --- .../openworld/general/ClassWithPublicFinalFields.java | 10 +++++----- .../openworld/stringelements/SimpleStringModel.java | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java index 4fd54b6252..8e2b589d43 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java @@ -1,16 +1,16 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.fixtures.immutability.openworld.general; -import org.opalj.fpcf.properties.immutability.classes.MutableClass; +import org.opalj.fpcf.properties.immutability.classes.TransitivelyImmutableClass; import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; -import org.opalj.fpcf.properties.immutability.fields.MutableField; +import org.opalj.fpcf.properties.immutability.fields.NonTransitivelyImmutableField; import org.opalj.fpcf.properties.immutability.types.MutableType; -@MutableType("The class has mutable fields") -@MutableClass("The class has mutable fields") +@MutableType("The type is extensible") +@TransitivelyImmutableClass("Class has no instance fields") public class ClassWithPublicFinalFields { - @MutableField("Field is assignable") + @NonTransitivelyImmutableField("Field is not assignable") @NonAssignableField("The field is public, final, and its value is only set once since it is static") public static final Object DEFAULT = new Object(); diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java index 0ea12bfa6c..ecf8543c51 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java @@ -3,7 +3,7 @@ import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.UnsafelyLazilyInitializedField; -import org.opalj.fpcf.properties.immutability.fields.NonTransitivelyImmutableField; +import org.opalj.fpcf.properties.immutability.fields.MutableField; import org.opalj.fpcf.properties.immutability.fields.TransitivelyImmutableField; import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.LazilyInitializedField; @@ -17,7 +17,7 @@ public final class SimpleStringModel { @TransitivelyImmutableField(value = "The array values are not mutated after the assignment ", analyses = {}) - @NonTransitivelyImmutableField(value = "The analysis can not recognize transitive immutable arrays", + @MutableField(value = "The analysis can not recognize transitive immutable arrays, and the field is assignable", analyses = { FieldImmutabilityAnalysis.class }) @NonAssignableField(value = "The field is final", analyses = {}) @AssignableField(value = "The field is written read and written in two different initializers", From 67c58da26a79ee657148f9657a2d1887686a2096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 19:32:17 +0100 Subject: [PATCH 16/41] Extract read to write path analysis part --- .../ClonePatternAnalysis.scala | 19 ++- .../ExtensiveReadWritePathAnalysis.scala | 146 ++++++++++++++++++ .../FieldAssignabilityAnalysisPart.scala | 14 +- .../L2FieldAssignabilityAnalysis.scala | 92 ++--------- .../LazyInitializationAnalysis.scala | 14 +- 5 files changed, 199 insertions(+), 86 deletions(-) create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala index 41eab293ab..0eec1faf86 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -22,11 +22,6 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites trait ClonePatternAnalysis private[fieldassignability] extends FieldAssignabilityAnalysisPart { - val considerLazyInitialization: Boolean = - project.config.getBoolean( - "org.opalj.fpcf.analyses.L2FieldAssignabilityAnalysis.considerLazyInitialization" - ) - /** * @note To be provided by the part user. */ @@ -48,7 +43,12 @@ trait ClonePatternAnalysis private[fieldassignability] */ protected val pathExists: (fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]) => Boolean - override def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] = None + override def completePatternWithInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None override def completePatternWithNonInitializerRead( context: Context, @@ -72,7 +72,12 @@ trait ClonePatternAnalysis private[fieldassignability] None } - override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = None + override def completePatternWithInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None override def completePatternWithNonInitializerWrite( context: Context, diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala new file mode 100644 index 0000000000..e28b9c73d3 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala @@ -0,0 +1,146 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability + +import org.opalj.br.PC +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.immutability.Assignable +import org.opalj.br.fpcf.properties.immutability.FieldAssignability +import org.opalj.br.fpcf.properties.immutability.NonAssignable +import org.opalj.tac.SelfReferenceParameter +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode +import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites + +/** + * Determines the assignability of a field based on whether it is possible for a path to exist from a read to a write + * of the same field on the same instance. + * + * This part will always return a value, and will soundly abort if it cannot guarantee that no such path exists. This + * may be the case e.g. if a path from some read of the field to the write exists, but the analysis cannot prove that + * the two instances are disjoint. + * + * @author Maximilian Rüsch + */ +trait ExtensiveReadWritePathAnalysis private[fieldassignability] + extends FieldAssignabilityAnalysisPart { + + /** + * @note To be provided by the part user. + */ + protected val isSameInstance: (tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V) => Answer + + /** + * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not + * make the field assignable in combination with the given write itself. + * + * @note To be provided by the part user. + */ + protected val isWrittenInstanceUseSiteSafe: (tac: TACode[TACMethodParameter, V], writePC: Int, use: Int) => Boolean + + /** + * Provided with two PCs, determines irreflexively whether there exists a path from the first PC to the second PC in + * the context of the provided TAC and attached CFG. + * + * @note To be provided by the part user. + */ + protected val pathExists: (fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]) => Boolean + + override def completePatternWithInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Some[FieldAssignability] = { + // Initializer reads are incompatible with arbitrary writes in other methods + if (state.nonInitializerWrites.nonEmpty) + return Some(Assignable); + + // Analyzing read-write paths interprocedurally is not supported yet. For static fields, initializer writes are + // harmless (i.e. executed before) when the read is in a constructor, so the class is already initialized. + if (state.initializerWrites.exists(_._1.method ne context.method)) { + if (state.field.isNotStatic) + return Some(Assignable); + else if (!context.method.definedMethod.isConstructor) + return Some(Assignable); + } + + val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { + case (writePC, _) => + pathExists(readPC, writePC, tac) + } + + if (pathFromReadToSomeWriteExists) + Some(Assignable) + else + Some(NonAssignable) + } + + override def completePatternWithNonInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None + + override def completePatternWithInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Some[FieldAssignability] = { + val method = context.method.definedMethod + if (state.field.isStatic && method.isConstructor || state.field.isNotStatic && method.isStaticInitializer) + return Some(Assignable); + + // For instance writes, there might be premature value escapes through arbitrary use sites. + // If we cannot guarantee that all use sites of the written instance are safe, we must soundly abort. + if (state.field.isNotStatic) { + // We only support modifying fields in initializers on newly created instances (which is always "this") + if (receiver.isEmpty || receiver.get.definedBy != SelfReferenceParameter) + return Some(Assignable); + + if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC, _))) + return Some(Assignable); + } + + // Analyzing read-write paths interprocedurally is not supported yet. However, for static fields, all + // reads that take place in instance constructors are harmless. + if (state.field.isNotStatic && state.initializerReads.exists(_._1.method ne context.method)) + return Some(Assignable); + if (state.field.isStatic && state.initializerReads.exists { + case (readContext, _) => + (readContext.method ne context.method) && ( + !readContext.method.hasSingleDefinedMethod || + readContext.method.definedMethod.isStaticInitializer + ) + } + ) + return Some(Assignable); + + val pathFromSomeReadToWriteExists = state.initializerReads(context).exists { + case (readPC, readReceiver) => + val readReceiverVar = readReceiver.map(uVarForDefSites(_, tac.pcToIndex)) + if (readReceiverVar.isDefined && isSameInstance(tac, receiver.get, readReceiverVar.get).isNo) { + false + } else { + pathExists(readPC, writePC, tac) + } + } + + if (pathFromSomeReadToWriteExists) + Some(Assignable) + else + Some(NonAssignable) + } + + override def completePatternWithNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala index d5dd6bd492..e746374cee 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala @@ -18,7 +18,12 @@ trait FieldAssignabilityAnalysisPart private[fieldassignability] type AnalysisState <: AbstractFieldAssignabilityAnalysisState - def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] + def completePatternWithInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] def completePatternWithNonInitializerRead( context: Context, @@ -27,7 +32,12 @@ trait FieldAssignabilityAnalysisPart private[fieldassignability] receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] - def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] + def completePatternWithInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] def completePatternWithNonInitializerWrite( context: Context, diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index f8c28e206c..a738688b76 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -16,7 +16,6 @@ import org.opalj.br.fpcf.properties.immutability.NonAssignable import org.opalj.tac.SelfReferenceParameter import org.opalj.tac.TACMethodParameter import org.opalj.tac.TACode -import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** * Determines the assignability of a field. @@ -47,10 +46,18 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som .isWrittenInstanceUseSiteSafe override val pathExists = L2FieldAssignabilityAnalysis.this.pathExists } + private class L2ReadWritePathPart(val project: SomeProject) extends ExtensiveReadWritePathAnalysis { + override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState + override val isSameInstance = L2FieldAssignabilityAnalysis.this.isSameInstance + override val isWrittenInstanceUseSiteSafe = L2FieldAssignabilityAnalysis.this + .isWrittenInstanceUseSiteSafe + override val pathExists = L2FieldAssignabilityAnalysis.this.pathExists + } override protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] = List( new L2LazyInitializationPart(project), - new L2ClonePatternPart(project) + new L2ClonePatternPart(project), + new L2ReadWritePathPart(project) ) override def analyzeInitializerRead( @@ -60,33 +67,11 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som receiver: Option[V] )(implicit state: State): FieldAssignability = { val assignability = determineAssignabilityFromParts(part => - part.completePatternWithInitializerRead()(using state.asInstanceOf[part.AnalysisState]) + part.completePatternWithInitializerRead(context, tac, readPC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) ) - if (assignability.contains(Assignable)) - return Assignable; - - // Initializer reads are incompatible with arbitrary writes in other methods - if (state.nonInitializerWrites.nonEmpty) - return Assignable; - - // Analyzing read-write paths interprocedurally is not supported yet. For static fields, initializer writes are - // harmless (i.e. executed before) when the read is in a constructor, so the class is already initialized. - if (state.initializerWrites.exists(_._1.method ne context.method)) { - if (state.field.isNotStatic) - return Assignable; - else if (!context.method.definedMethod.isConstructor) - return Assignable; - } - - val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { - case (writePC, _) => - pathExists(readPC, writePC, tac) - } - - if (pathFromReadToSomeWriteExists) - Assignable - else - NonAssignable + assignability.getOrElse(NonAssignable) } override def analyzeNonInitializerRead( @@ -110,54 +95,11 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som receiver: Option[V] )(implicit state: State): FieldAssignability = { val assignability = determineAssignabilityFromParts(part => - part.completePatternWithInitializerWrite()(using state.asInstanceOf[part.AnalysisState]) - ) - if (assignability.contains(Assignable)) - return Assignable; - - val method = context.method.definedMethod - if (state.field.isStatic && method.isConstructor || state.field.isNotStatic && method.isStaticInitializer) - return Assignable; // TODO check this generally above - - // For instance writes, there might be premature value escapes through arbitrary use sites. - // If we cannot guarantee that all use sites of the written instance are safe, we must soundly abort. - if (state.field.isNotStatic) { - // We only support modifying fields in initializers on newly created instances (which is always "this") - if (receiver.isEmpty || receiver.get.definedBy != SelfReferenceParameter) - return Assignable; - - if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC, _))) - return Assignable; - } - - // Analyzing read-write paths interprocedurally is not supported yet. However, for static fields, all - // reads that take place in instance constructors are harmless. - if (state.field.isNotStatic && state.initializerReads.exists(_._1.method ne context.method)) - return Assignable; - if (state.field.isStatic && state.initializerReads.exists { - case (readContext, _) => - (readContext.method ne context.method) && ( - !readContext.method.hasSingleDefinedMethod || - readContext.method.definedMethod.isStaticInitializer - ) - } + part.completePatternWithInitializerWrite(context, tac, writePC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) ) - return Assignable; - - val pathFromSomeReadToWriteExists = state.initializerReads(context).exists { - case (readPC, readReceiver) => - val readReceiverVar = readReceiver.map(uVarForDefSites(_, tac.pcToIndex)) - if (readReceiverVar.isDefined && isSameInstance(tac, receiver.get, readReceiverVar.get).isNo) { - false - } else { - pathExists(readPC, writePC, tac) - } - } - - if (pathFromSomeReadToWriteExists) - Assignable - else - NonAssignable + assignability.getOrElse(Assignable) } override def analyzeNonInitializerWrite( diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala index ed488a5b7b..067453ae63 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala @@ -107,7 +107,12 @@ trait LazyInitializationAnalysis private[fieldassignability] bbPotentiallyDominator == bbPotentiallyDominated && potentiallyDominatorIndex < potentiallyDominatedIndex } - override def completePatternWithInitializerRead()(implicit state: AnalysisState): Option[FieldAssignability] = + override def completePatternWithInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = state.potentialLazyInit.map(_ => Assignable) override def completePatternWithNonInitializerRead( @@ -134,7 +139,12 @@ trait LazyInitializationAnalysis private[fieldassignability] None } - override def completePatternWithInitializerWrite()(implicit state: AnalysisState): Option[FieldAssignability] = None + override def completePatternWithInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None override def completePatternWithNonInitializerWrite( context: Context, From 2de90e25d61d5a0e004b5c5af79cebe0e32f3466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 19:37:23 +0100 Subject: [PATCH 17/41] Completely rely on part system --- .../AbstractFieldAssignabilityAnalysis.scala | 46 +++++++++++--- .../L0FieldAssignabilityAnalysis.scala | 35 ----------- .../L2FieldAssignabilityAnalysis.scala | 61 ------------------- 3 files changed, 37 insertions(+), 105 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index 9bccec5910..9a03b260f5 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -86,7 +86,7 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] - protected def determineAssignabilityFromParts( + private def determineAssignabilityFromParts( partFunc: FieldAssignabilityAnalysisPart => Option[FieldAssignability] ): Option[FieldAssignability] = { var assignability: Option[FieldAssignability] = None @@ -148,33 +148,61 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { handleWriteAccessInformation(propertyStore(declaredFields(field), FieldWriteAccessInformation.key))(using state) } - def analyzeInitializerRead( + private def analyzeInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: PC, receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability + )(implicit state: AnalysisState): FieldAssignability = { + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithInitializerRead(context, tac, readPC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) + ) + assignability.getOrElse(NonAssignable) + } - def analyzeNonInitializerRead( + private def analyzeNonInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: PC, receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability + )(implicit state: AnalysisState): FieldAssignability = { + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithNonInitializerRead(context, tac, readPC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) + ) + assignability.getOrElse(NonAssignable) + } - def analyzeInitializerWrite( + private def analyzeInitializerWrite( context: Context, tac: TACode[TACMethodParameter, V], writePC: PC, receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability + )(implicit state: AnalysisState): FieldAssignability = { + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithInitializerWrite(context, tac, writePC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) + ) + assignability.getOrElse(Assignable) + } - def analyzeNonInitializerWrite( + private def analyzeNonInitializerWrite( context: Context, tac: TACode[TACMethodParameter, V], writePC: PC, receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability + )(implicit state: AnalysisState): FieldAssignability = { + val assignability = determineAssignabilityFromParts(part => + part.completePatternWithNonInitializerWrite(context, tac, writePC, receiver)(using + state.asInstanceOf[part.AnalysisState] + ) + ) + assignability.getOrElse(Assignable) + } def createResult()(implicit state: AnalysisState): ProperPropertyComputationResult = { if (state.hasDependees && (state.fieldAssignability ne Assignable)) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index ccf4f15dc7..10746e7e39 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -6,14 +6,7 @@ package analyses package fieldassignability import org.opalj.br.Field -import org.opalj.br.PC import org.opalj.br.analyses.SomeProject -import org.opalj.br.fpcf.properties.Context -import org.opalj.br.fpcf.properties.immutability.Assignable -import org.opalj.br.fpcf.properties.immutability.FieldAssignability -import org.opalj.br.fpcf.properties.immutability.NonAssignable -import org.opalj.tac.TACMethodParameter -import org.opalj.tac.TACode /** * A field assignability analysis that treats every field access (reads and writes) as unsafe and thus immediately @@ -30,34 +23,6 @@ class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: Som override def createState(field: Field): AnalysisState = State(field) override protected lazy val parts = Seq.empty[FieldAssignabilityAnalysisPart] - - override def analyzeInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability = Assignable - - override def analyzeNonInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability = NonAssignable - - override def analyzeInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability = NonAssignable // TODO handle constructor escapes - - override def analyzeNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): FieldAssignability = Assignable } object EagerL0FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index a738688b76..bd1b2dec43 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -6,13 +6,8 @@ package analyses package fieldassignability import org.opalj.br.Field -import org.opalj.br.PC import org.opalj.br.analyses.SomeProject import org.opalj.br.fpcf.FPCFAnalysis -import org.opalj.br.fpcf.properties.Context -import org.opalj.br.fpcf.properties.immutability.Assignable -import org.opalj.br.fpcf.properties.immutability.FieldAssignability -import org.opalj.br.fpcf.properties.immutability.NonAssignable import org.opalj.tac.SelfReferenceParameter import org.opalj.tac.TACMethodParameter import org.opalj.tac.TACode @@ -60,62 +55,6 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som new L2ReadWritePathPart(project) ) - override def analyzeInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: State): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithInitializerRead(context, tac, readPC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(NonAssignable) - } - - override def analyzeNonInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: State): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithNonInitializerRead(context, tac, readPC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(NonAssignable) - } - - override def analyzeInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: State): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithInitializerWrite(context, tac, writePC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(Assignable) - } - - override def analyzeNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: State): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithNonInitializerWrite(context, tac, writePC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(Assignable) - } - /** * Identifies easy cases of definitively answering instance equality, considering that (at least) as long as DU-UD * chains are not collapsed in TAC one cannot be sure that two instances are disjoint, i.e. that they are different. From 7c226f3bbdb0afc4c7423813aa8e867fd2321c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 19:52:47 +0100 Subject: [PATCH 18/41] Initialize L0 with simple read write path analysis --- .../AbstractFieldAssignabilityAnalysis.scala | 14 --- .../ClonePatternAnalysis.scala | 21 ---- .../ExtensiveReadWritePathAnalysis.scala | 21 ---- .../FieldAssignabilityAnalysisPart.scala | 66 +++++++++++ .../L0FieldAssignabilityAnalysis.scala | 8 +- .../L2FieldAssignabilityAnalysis.scala | 70 +----------- .../SimpleReadWritePathAnalysis.scala | 106 ++++++++++++++++++ 7 files changed, 182 insertions(+), 124 deletions(-) create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index 9a03b260f5..435a90ff7d 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -367,20 +367,6 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { createResult() } - - /** - * Provided with two PCs, determines whether there exists a path from the first PC to the second PC in the context - * of the provided TAC and attached CFG. This check is not reflexive. - * - * IMPROVE abort on path found instead of computing all reachable BBs - */ - protected def pathExists(fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]): Boolean = { - val firstBB = tac.cfg.bb(tac.pcToIndex(fromPC)) - val secondBB = tac.cfg.bb(tac.pcToIndex(toPC)) - - if (firstBB == secondBB) fromPC < toPC - else firstBB.reachable().contains(secondBB) - } } sealed trait AbstractFieldAssignabilityAnalysisScheduler extends FPCFAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala index 0eec1faf86..add7e8c17b 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -22,27 +22,6 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites trait ClonePatternAnalysis private[fieldassignability] extends FieldAssignabilityAnalysisPart { - /** - * @note To be provided by the part user. - */ - protected val isSameInstance: (tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V) => Answer - - /** - * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not - * make the field assignable in combination with the given write itself. - * - * @note To be provided by the part user. - */ - protected val isWrittenInstanceUseSiteSafe: (tac: TACode[TACMethodParameter, V], writePC: Int, use: Int) => Boolean - - /** - * Provided with two PCs, determines irreflexively whether there exists a path from the first PC to the second PC in - * the context of the provided TAC and attached CFG. - * - * @note To be provided by the part user. - */ - protected val pathExists: (fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]) => Boolean - override def completePatternWithInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala index e28b9c73d3..9bae358531 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala @@ -28,27 +28,6 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites trait ExtensiveReadWritePathAnalysis private[fieldassignability] extends FieldAssignabilityAnalysisPart { - /** - * @note To be provided by the part user. - */ - protected val isSameInstance: (tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V) => Answer - - /** - * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not - * make the field assignable in combination with the given write itself. - * - * @note To be provided by the part user. - */ - protected val isWrittenInstanceUseSiteSafe: (tac: TACode[TACMethodParameter, V], writePC: Int, use: Int) => Boolean - - /** - * Provided with two PCs, determines irreflexively whether there exists a path from the first PC to the second PC in - * the context of the provided TAC and attached CFG. - * - * @note To be provided by the part user. - */ - protected val pathExists: (fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]) => Boolean - override def completePatternWithInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala index e746374cee..fa08f1b007 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala @@ -45,4 +45,70 @@ trait FieldAssignabilityAnalysisPart private[fieldassignability] writePC: PC, receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] + + /** + * Provided with two PCs, determines whether there exists a path from the first PC to the second PC in the context + * of the provided TAC and attached CFG. This check is not reflexive. + * + * IMPROVE abort on path found instead of computing all reachable BBs + */ + protected def pathExists(fromPC: Int, toPC: Int, tac: TACode[TACMethodParameter, V]): Boolean = { + val firstBB = tac.cfg.bb(tac.pcToIndex(fromPC)) + val secondBB = tac.cfg.bb(tac.pcToIndex(toPC)) + + if (firstBB == secondBB) fromPC < toPC + else firstBB.reachable().contains(secondBB) + } + + /** + * Identifies easy cases of definitively answering instance equality, considering that (at least) as long as DU-UD + * chains are not collapsed in TAC one cannot be sure that two instances are disjoint, i.e. that they are different. + */ + protected def isSameInstance(tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V): Answer = { + def isFresh(value: V): Boolean = { + value.definedBy.size == 1 && { + val defSite = value.definedBy.head + defSite >= 0 && tac.stmts(defSite).asAssignment.expr.isNew + } + } + + // The two given instances may be categorized into: 1. fresh, 2. this, 3. easy case of formal parameter, + // 4. none of the above. If both instances fall into some category 1-3, but not into the same, they are secured + // to be disjoint. + val instanceInfo = Seq( + (isFresh(firstVar), isFresh(secondVar)), + (firstVar.definedBy == SelfReferenceParameter, secondVar.definedBy == SelfReferenceParameter), + (firstVar.definedBy.forall(_ < OriginOfThis), secondVar.definedBy.forall(_ < OriginOfThis)) + ) + if (instanceInfo.contains((true, false)) && + instanceInfo.contains((false, true)) && + !instanceInfo.contains(true, true) + ) { + No + } else if (firstVar.definedBy.intersect(secondVar.definedBy).nonEmpty) + Yes + else + Unknown + } + + /** + * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not + * make the field assignable in combination with the given write itself. + */ + protected def isWrittenInstanceUseSiteSafe(tac: TACode[TACMethodParameter, V], writePC: Int, use: Int): Boolean = { + val stmt = tac.stmts(use) + + // We consider the access safe when ... + stmt.pc == writePC || // ... the use site is a known write OR ... + // ... we easily identify the use site to be a field update OR ... + stmt.isFieldWriteAccessStmt || + // ... we identify easy instances of field reads (interaction with potentially disruptive reads + // is checked separately; note that this inference relies on a flat TAC) OR ... + stmt.isAssignment && stmt.asAssignment.expr.isGetField || + // ... we easily identify the use site to initialize an object (fine as initializer reads outside the + // current method are forbidden) OR ... + stmt.isMethodCall && stmt.asMethodCall.name == "" || + // ... the statement is irrelevant, since there is no instance where the write did not take effect yet + !pathExists(stmt.pc, writePC, tac) + } } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index 10746e7e39..13e81e9b54 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -22,7 +22,13 @@ class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: Som type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) - override protected lazy val parts = Seq.empty[FieldAssignabilityAnalysisPart] + private class L0ReadWritePathPart(val project: SomeProject) extends SimpleReadWritePathAnalysis { + override type AnalysisState = L0FieldAssignabilityAnalysis.this.AnalysisState + } + + override protected lazy val parts = Seq( + L0ReadWritePathPart(project) + ) } object EagerL0FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index bd1b2dec43..991ea869e2 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -8,9 +8,6 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject import org.opalj.br.fpcf.FPCFAnalysis -import org.opalj.tac.SelfReferenceParameter -import org.opalj.tac.TACMethodParameter -import org.opalj.tac.TACode /** * Determines the assignability of a field. @@ -36,77 +33,16 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som } private class L2ClonePatternPart(val project: SomeProject) extends ClonePatternAnalysis { override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState - override val isSameInstance = L2FieldAssignabilityAnalysis.this.isSameInstance - override val isWrittenInstanceUseSiteSafe = L2FieldAssignabilityAnalysis.this - .isWrittenInstanceUseSiteSafe - override val pathExists = L2FieldAssignabilityAnalysis.this.pathExists } private class L2ReadWritePathPart(val project: SomeProject) extends ExtensiveReadWritePathAnalysis { override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState - override val isSameInstance = L2FieldAssignabilityAnalysis.this.isSameInstance - override val isWrittenInstanceUseSiteSafe = L2FieldAssignabilityAnalysis.this - .isWrittenInstanceUseSiteSafe - override val pathExists = L2FieldAssignabilityAnalysis.this.pathExists } override protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] = List( - new L2LazyInitializationPart(project), - new L2ClonePatternPart(project), - new L2ReadWritePathPart(project) + L2LazyInitializationPart(project), + L2ClonePatternPart(project), + L2ReadWritePathPart(project) ) - - /** - * Identifies easy cases of definitively answering instance equality, considering that (at least) as long as DU-UD - * chains are not collapsed in TAC one cannot be sure that two instances are disjoint, i.e. that they are different. - */ - private def isSameInstance(tac: TACode[TACMethodParameter, V], firstVar: V, secondVar: V): Answer = { - def isFresh(value: V): Boolean = { - value.definedBy.size == 1 && { - val defSite = value.definedBy.head - defSite >= 0 && tac.stmts(defSite).asAssignment.expr.isNew - } - } - - // The two given instances may be categorized into: 1. fresh, 2. this, 3. easy case of formal parameter, - // 4. none of the above. If both instances fall into some category 1-3, but not into the same, they are secured - // to be disjoint. - val instanceInfo = Seq( - (isFresh(firstVar), isFresh(secondVar)), - (firstVar.definedBy == SelfReferenceParameter, secondVar.definedBy == SelfReferenceParameter), - (firstVar.definedBy.forall(_ < OriginOfThis), secondVar.definedBy.forall(_ < OriginOfThis)) - ) - if (instanceInfo.contains((true, false)) && - instanceInfo.contains((false, true)) && - !instanceInfo.contains(true, true) - ) { - No - } else if (firstVar.definedBy.intersect(secondVar.definedBy).nonEmpty) - Yes - else - Unknown - } - - /** - * Determines whether a use site of a written instance is "safe", i.e. is recognizable as a pattern that does not - * make the field assignable in combination with the given write itself. - */ - private def isWrittenInstanceUseSiteSafe(tac: TACode[TACMethodParameter, V], writePC: Int, use: Int): Boolean = { - val stmt = tac.stmts(use) - - // We consider the access safe when ... - stmt.pc == writePC || // ... the use site is a known write OR ... - // ... we easily identify the use site to be a field update OR ... - // IMPROVE: Can we use field access information to care about reflective accesses here? - stmt.isFieldWriteAccessStmt || - // ... we identify easy instances of field reads (interaction with potentially disruptive reads - // is checked separately; note that this inference relies on a flat TAC) OR ... - stmt.isAssignment && stmt.asAssignment.expr.isGetField || - // ... we easily identify the use site to initialize an object (fine as initializer reads outside the - // current method are forbidden) OR ... - stmt.isMethodCall && stmt.asMethodCall.name == "" || - // ... the statement is irrelevant, since there is no instance where the write did not take effect yet - !pathExists(stmt.pc, writePC, tac) - } } object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala new file mode 100644 index 0000000000..f81ab8325b --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala @@ -0,0 +1,106 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability + +import org.opalj.br.PC +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.immutability.Assignable +import org.opalj.br.fpcf.properties.immutability.FieldAssignability +import org.opalj.br.fpcf.properties.immutability.NonAssignable +import org.opalj.tac.SelfReferenceParameter +import org.opalj.tac.TACMethodParameter +import org.opalj.tac.TACode + +/** + * Determines the assignability of a field based on whether it is possible for a path to exist from a read to a write + * of the same field on the same instance. + * + * This part will always return a value, and will soundly abort if it cannot guarantee that no such path exists. This + * may be the case e.g. if a path from some read of the field to the write exists, but the analysis cannot prove that + * the two instances are disjoint. + * + * @author Maximilian Rüsch + */ +trait SimpleReadWritePathAnalysis private[fieldassignability] + extends FieldAssignabilityAnalysisPart { + + override def completePatternWithInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Some[FieldAssignability] = { + // Initializer reads are incompatible with arbitrary writes in other methods + if (state.nonInitializerWrites.nonEmpty) + return Some(Assignable); + + // Analyzing read-write paths interprocedurally is not supported yet. For static fields, initializer writes are + // harmless (i.e. executed before) when the read is in a constructor, so the class is already initialized. + if (state.initializerWrites.exists(_._1.method ne context.method)) + return Some(Assignable); + + val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { + case (writePC, _) => + pathExists(readPC, writePC, tac) + } + + if (pathFromReadToSomeWriteExists) + Some(Assignable) + else + Some(NonAssignable) + } + + override def completePatternWithNonInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None + + override def completePatternWithInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Some[FieldAssignability] = { + val method = context.method.definedMethod + if (state.field.isStatic && method.isConstructor || state.field.isNotStatic && method.isStaticInitializer) + return Some(Assignable); + + // For instance writes, there might be premature value escapes through arbitrary use sites. + // If we cannot guarantee that all use sites of the written instance are safe, we must soundly abort. + if (state.field.isNotStatic) { + // We only support modifying fields in initializers on newly created instances (which is always "this") + if (receiver.isEmpty || receiver.get.definedBy != SelfReferenceParameter) + return Some(Assignable); + + if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC, _))) + return Some(Assignable); + } + + // Analyzing read-write paths interprocedurally is not supported yet. However, for static fields, all + // reads that take place in instance constructors are harmless. + if (state.initializerReads.exists(_._1.method ne context.method)) + return Some(Assignable); + + val pathFromSomeReadToWriteExists = state.initializerReads(context).exists { + case (readPC, _) => + pathExists(readPC, writePC, tac) + } + + if (pathFromSomeReadToWriteExists) + Some(Assignable) + else + Some(NonAssignable) + } + + override def completePatternWithNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = None +} From b6755d2a0ce6c6113e83e71922b61a2e66cc570e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 20:10:53 +0100 Subject: [PATCH 19/41] Reenable L1 assignability analysis --- .../opalj/support/info/PureVoidMethods.scala | 3 +- .../scala/org/opalj/support/info/Purity.scala | 5 ++- .../opalj/support/info/UnusedResults.scala | 3 +- .../scala/org/opalj/fpcf/PurityTests.scala | 3 +- .../tac/fpcf/analyses/L1PuritySmokeTest.scala | 3 +- OPAL/tac/src/main/resources/reference.conf | 5 +++ .../L1FieldAssignabilityAnalysis.scala | 43 +++++++++++++++++++ .../L2FieldAssignabilityAnalysis.scala | 8 ++-- 8 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala index 4b984379ba..d8e90e2aa2 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/PureVoidMethods.scala @@ -25,6 +25,7 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL2PurityAnalysis /** @@ -59,7 +60,7 @@ object PureVoidMethods extends ProjectsAnalysisApplication { LazyInterProceduralEscapeAnalysis, LazyReturnValueFreshnessAnalysis, LazyFieldLocalityAnalysis, - // TODO reincorporate LazyL1FieldAssignabilityAnalysis, + LazyL1FieldAssignabilityAnalysis, LazyClassImmutabilityAnalysis, LazyTypeImmutabilityAnalysis, EagerL2PurityAnalysis diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala index 5d0f1593ca..99f26a0934 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/Purity.scala @@ -59,6 +59,7 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL0FieldAssignabilityAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.L1PurityAnalysis import org.opalj.tac.fpcf.analyses.purity.L2PurityAnalysis import org.opalj.tac.fpcf.analyses.purity.LazyL1PurityAnalysis @@ -135,8 +136,8 @@ object Purity extends ProjectsAnalysisApplication { case Some(fA) => support ::= getScheduler(fA, eager) case None => analysis match { case LazyL0PurityAnalysis => support ::= LazyL0FieldAssignabilityAnalysis - case LazyL1PurityAnalysis => support ::= LazyL0FieldAssignabilityAnalysis // TODO find LazyL1FieldAssignabilityAnalysis - case LazyL2PurityAnalysis => support ::= LazyL0FieldAssignabilityAnalysis // TODO find LazyL1FieldAssignabilityAnalysis + case LazyL1PurityAnalysis => support ::= LazyL1FieldAssignabilityAnalysis + case LazyL2PurityAnalysis => support ::= LazyL1FieldAssignabilityAnalysis } } diff --git a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala index 63562bba9b..6d0b249a1a 100644 --- a/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala +++ b/DEVELOPING_OPAL/tools/src/main/scala/org/opalj/support/info/UnusedResults.scala @@ -40,6 +40,7 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL2PurityAnalysis import org.opalj.tac.fpcf.properties.TACAI import org.opalj.value.ValueInformation @@ -82,7 +83,7 @@ object UnusedResults extends ProjectsAnalysisApplication { LazyInterProceduralEscapeAnalysis, LazyReturnValueFreshnessAnalysis, LazyFieldLocalityAnalysis, - // TODO find LazyL1FieldAssignabilityAnalysis, + LazyL1FieldAssignabilityAnalysis, LazyClassImmutabilityAnalysis, LazyTypeImmutabilityAnalysis, EagerL2PurityAnalysis diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala index 4a3a74cbf8..521bf3a690 100644 --- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/PurityTests.scala @@ -19,6 +19,7 @@ import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL0FieldAssignabilityAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.LazyL2FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL1PurityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL2PurityAnalysis @@ -71,7 +72,7 @@ class PurityTests extends PropertiesTest { val as = executeAnalyses( Set( EagerL1PurityAnalysis, - // TODO find LazyL1FieldAssignabilityAnalysis, + LazyL1FieldAssignabilityAnalysis, LazyFieldImmutabilityAnalysis, LazyClassImmutabilityAnalysis, LazyTypeImmutabilityAnalysis, diff --git a/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala b/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala index 253653e9c2..3c913338a3 100644 --- a/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala +++ b/OPAL/tac/src/it/scala/org/opalj/tac/fpcf/analyses/L1PuritySmokeTest.scala @@ -21,6 +21,7 @@ import org.opalj.fpcf.FPCFAnalysis import org.opalj.fpcf.PropertyStoreKey import org.opalj.tac.cg.RTACallGraphKey import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.purity.EagerL1PurityAnalysis import org.opalj.util.Nanoseconds import org.opalj.util.PerformanceEvaluation.time @@ -44,7 +45,7 @@ class L1PuritySmokeTest extends AnyFunSpec with Matchers { val supportAnalyses: Set[ComputationSpecification[FPCFAnalysis]] = Set( EagerFieldAccessInformationAnalysis, - // TODO find EagerL1FieldAssignabilityAnalysis, + EagerL1FieldAssignabilityAnalysis, EagerFieldImmutabilityAnalysis, EagerClassImmutabilityAnalysis, EagerTypeImmutabilityAnalysis diff --git a/OPAL/tac/src/main/resources/reference.conf b/OPAL/tac/src/main/resources/reference.conf index af6259fb7c..d02069a03d 100644 --- a/OPAL/tac/src/main/resources/reference.conf +++ b/OPAL/tac/src/main/resources/reference.conf @@ -17,6 +17,11 @@ org.opalj { eagerFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.EagerL0FieldAssignabilityAnalysis", lazyFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.LazyL0FieldAssignabilityAnalysis" }, + "L1FieldAssignabilityAnalysis" { + description = "Determines the assignability of (instance and static) fields.", + eagerFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.EagerL1FieldAssignabilityAnalysis", + lazyFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.LazyL1FieldAssignabilityAnalysis" + }, "L2FieldAssignabilityAnalysis" { description = "Determines the assignability of (instance and static) fields.", eagerFactory = "org.opalj.tac.fpcf.analyses.fieldassignability.EagerL2FieldAssignabilityAnalysis", diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala new file mode 100644 index 0000000000..27f0de83ff --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -0,0 +1,43 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability + +import org.opalj.br.Field +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.FPCFAnalysis + +/** + * Determines the assignability of a field. + * + * @note Requires that the 3-address code's expressions are not deeply nested + * + * @author Maximilian Rüsch + */ +class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) + extends AbstractFieldAssignabilityAnalysis + with FPCFAnalysis { + + case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState + with LazyInitializationAnalysisState + type AnalysisState = State + override def createState(field: Field): AnalysisState = State(field) + + private class L1ReadWritePathPart(val project: SomeProject) extends ExtensiveReadWritePathAnalysis { + override type AnalysisState = L1FieldAssignabilityAnalysis.this.AnalysisState + } + + override protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] = List( + L1ReadWritePathPart(project) + ) +} + +object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) +} + +object LazyL2FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 991ea869e2..e80c0f0cde 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -45,10 +45,10 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som ) } -object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { - override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) +object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) } -object LazyL2FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { - override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) +object LazyL1FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) } From 9ba7688f212f70b9b0f319714dec3af7873de9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 20:11:00 +0100 Subject: [PATCH 20/41] Fix formatting --- .../fieldassignability/FieldAssignabilityAnalysisPart.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala index fa08f1b007..a2900c1041 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala @@ -79,7 +79,7 @@ trait FieldAssignabilityAnalysisPart private[fieldassignability] (isFresh(firstVar), isFresh(secondVar)), (firstVar.definedBy == SelfReferenceParameter, secondVar.definedBy == SelfReferenceParameter), (firstVar.definedBy.forall(_ < OriginOfThis), secondVar.definedBy.forall(_ < OriginOfThis)) - ) + ) if (instanceInfo.contains((true, false)) && instanceInfo.contains((false, true)) && !instanceInfo.contains(true, true) From b8f72f9a4ec1c7ea2dfc6e1730e550bc684ded33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 20:14:01 +0100 Subject: [PATCH 21/41] Fix documentation of clone pattern analysis --- .../analyses/fieldassignability/ClonePatternAnalysis.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala index add7e8c17b..17216df656 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -13,7 +13,8 @@ import org.opalj.br.fpcf.properties.immutability.NonAssignable import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** - * Determines whether a field write access corresponds to a lazy initialization of the field. + * Determines the assignability of a field based on whether a clone pattern is detected. Aborts the analysis when + * rejecting a clone pattern match would lead to unsoundness based on the available properties. * * @note Requires that the 3-address code's expressions are not deeply nested. * From 1a402dc9c28be5f12751c884185e462ecc65a0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Thu, 13 Nov 2025 20:30:10 +0100 Subject: [PATCH 22/41] Add more elaborate documentation --- .../AbstractFieldAssignabilityAnalysis.scala | 8 ++++++-- .../fieldassignability/ClonePatternAnalysis.scala | 2 -- .../L0FieldAssignabilityAnalysis.scala | 5 +++-- .../L1FieldAssignabilityAnalysis.scala | 6 ++++-- .../L2FieldAssignabilityAnalysis.scala | 10 ++++------ 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index 435a90ff7d..ee00dc5144 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -75,9 +75,13 @@ trait AbstractFieldAssignabilityAnalysisState { } /** - * TODO document + * Base trait for all field assignability analyses. Analyses are comprised of a sequence of + * [[FieldAssignabilityAnalysisPart]], which are provided one-by-one with accesses of the field under analysis. * - * @note This analysis is only ''soundy'' if the project does not contain native methods. + * @note Analysis derived from this trait are only ''soundy'' if the project does not contain native methods, as long as + * the [[FieldReadAccessInformation]] and [[FieldWriteAccessInformation]] do not recognize such accesses. + * + * @see [[FieldAssignabilityAnalysisPart]] * * @author Maximilian Rüsch * @author Dominik Helm diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala index 17216df656..365d3bd054 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala @@ -16,8 +16,6 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites * Determines the assignability of a field based on whether a clone pattern is detected. Aborts the analysis when * rejecting a clone pattern match would lead to unsoundness based on the available properties. * - * @note Requires that the 3-address code's expressions are not deeply nested. - * * @author Maximilian Rüsch */ trait ClonePatternAnalysis private[fieldassignability] diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index 13e81e9b54..b74b1f290b 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -9,8 +9,9 @@ import org.opalj.br.Field import org.opalj.br.analyses.SomeProject /** - * A field assignability analysis that treats every field access (reads and writes) as unsafe and thus immediately - * marks the field as assignable if one such read / write is detected. + * Determines the assignability of a field based on a simple analysis of read -> write paths. + * + * @note May soundly overapproximate the assignability if the TAC is deeply nested. * * @author Maximilian Rüsch * @author Dominik Helm diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala index 27f0de83ff..19a0e94240 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -10,11 +10,13 @@ import org.opalj.br.analyses.SomeProject import org.opalj.br.fpcf.FPCFAnalysis /** - * Determines the assignability of a field. + * Determines the assignability of a field based on a more complex analysis of read-write paths than + * [[L0FieldAssignabilityAnalysis]]. * - * @note Requires that the 3-address code's expressions are not deeply nested + * @note May soundly overapproximate the assignability if the TAC is deeply nested. * * @author Maximilian Rüsch + * @author Dominik Helm */ class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index e80c0f0cde..28db99390f 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -10,13 +10,11 @@ import org.opalj.br.analyses.SomeProject import org.opalj.br.fpcf.FPCFAnalysis /** - * Determines the assignability of a field. + * Determines the assignability of a field based on a more complex analysis of read-write paths than + * [[L0FieldAssignabilityAnalysis]], and recognizes lazy initialization and clone / factory patterns as safe. + * + * @note Requires that the 3-address code's expressions are not deeply nested; see [[LazyInitializationAnalysis]]. * - * @note Requires that the 3-address code's expressions are not deeply nested. - * @author Tobias Roth - * @author Dominik Helm - * @author Florian Kübler - * @author Michael Eichberg * @author Maximilian Rüsch */ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) From b8555c3349cc5c8b26f812d672ac4b409efaf60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Fri, 14 Nov 2025 00:28:01 +0100 Subject: [PATCH 23/41] Refine support for interprocedural read -> write path analysis --- .../assignability/DesugaredEnumUsage.java | 32 ++++++++++ .../ExtensiveReadWritePathAnalysis.scala | 58 +++++++++++++------ 2 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DesugaredEnumUsage.java diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DesugaredEnumUsage.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DesugaredEnumUsage.java new file mode 100644 index 0000000000..3b406fc6e3 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DesugaredEnumUsage.java @@ -0,0 +1,32 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.assignability; + +import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; + +/** + * A case of a "desugared enum" usage. The Java 8 (bytecode 52.0) compiler generates a switch table in a synthetic inner + * class that is populated in a static initializer. As such, the enum values are static fields that are written in their + * own static initializer and read in another static initializer. + */ +public class DesugaredEnumUsage { + + enum MyDesugaredEnum { + @NonAssignableField("Static fields generated to hold enum value singletons are final") + SOME_VALUE, + + @NonAssignableField("Static fields generated to hold enum value singletons are final") + SOME_OTHER_VALUE + } + + void iDesugarYourEnum(MyDesugaredEnum e) { + switch (e) { + case SOME_VALUE: + System.out.println("Some value found!"); + case SOME_OTHER_VALUE: + System.out.println("Some other value found!"); + default: + System.err.println("No value found!"); + } + } +} + diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala index 9bae358531..c0eb2e195c 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala @@ -28,6 +28,34 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites trait ExtensiveReadWritePathAnalysis private[fieldassignability] extends FieldAssignabilityAnalysisPart { + /** + * Analyzing read-write paths interprocedurally is not fully supported yet. Instead, this identifies particular + * cases where read-write paths are provably impossible, given the information present in the framework / TAC. + * + * IMPROVE: The analysis of these accesses does not change over time, thus cache this derivation in the state + * + * @note Assumes that the two contexts point to different methods, i.e. that intraprocedural path existence is + * handled separately. + */ + private def canPathExistFromInitializerReadsToInitializerWrites( + readContext: Context, + writeContext: Context + )(implicit state: AnalysisState): Boolean = { + if (state.field.isStatic) { + // Writes to static fields in instance constructors are forbidden, see the write analysis. + + // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before + // the reading method, which is in turn only guaranteed when the writing static initializer is in the same + // type as the field declaration, i.e. it is run when the field is used for the first time. + + // Note that this also implies that there may be no supertype static initializer that can read the field, + // preventing read-write paths in the inheritance chain. + writeContext.method.declaringClassType ne state.field.classFile.thisType + } else { + true + } + } + override def completePatternWithInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], @@ -38,15 +66,16 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] if (state.nonInitializerWrites.nonEmpty) return Some(Assignable); - // Analyzing read-write paths interprocedurally is not supported yet. For static fields, initializer writes are - // harmless (i.e. executed before) when the read is in a constructor, so the class is already initialized. - if (state.initializerWrites.exists(_._1.method ne context.method)) { - if (state.field.isNotStatic) - return Some(Assignable); - else if (!context.method.definedMethod.isConstructor) - return Some(Assignable); - } + // First, check whether there are any interprocedural writes where paths are not excluded from existence + if (state.initializerWrites.exists { + case (writeContext, _) => + (writeContext.method ne context.method) && + canPathExistFromInitializerReadsToInitializerWrites(context, writeContext) + } + ) + return Some(Assignable); + // Second, check read-write paths intraprocedurally, context sensitive to the same access context as the read val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { case (writePC, _) => pathExists(readPC, writePC, tac) @@ -72,6 +101,7 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] receiver: Option[V] )(implicit state: AnalysisState): Some[FieldAssignability] = { val method = context.method.definedMethod + // Writing fields outside their initialization scope is not supported. if (state.field.isStatic && method.isConstructor || state.field.isNotStatic && method.isStaticInitializer) return Some(Assignable); @@ -86,16 +116,10 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] return Some(Assignable); } - // Analyzing read-write paths interprocedurally is not supported yet. However, for static fields, all - // reads that take place in instance constructors are harmless. - if (state.field.isNotStatic && state.initializerReads.exists(_._1.method ne context.method)) - return Some(Assignable); - if (state.field.isStatic && state.initializerReads.exists { + if (state.initializerReads.exists { case (readContext, _) => - (readContext.method ne context.method) && ( - !readContext.method.hasSingleDefinedMethod || - readContext.method.definedMethod.isStaticInitializer - ) + (readContext.method ne context.method) && + canPathExistFromInitializerReadsToInitializerWrites(readContext, context) } ) return Some(Assignable); From 14ea7d2b2349481cb2189b06b5de81ff3a7f1e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Fri, 14 Nov 2025 00:47:07 +0100 Subject: [PATCH 24/41] Collate read write path analysis into single trait --- ....scala => ReadWritePathAnalysisPart.scala} | 73 +++++++----- .../SimpleReadWritePathAnalysis.scala | 106 ------------------ 2 files changed, 48 insertions(+), 131 deletions(-) rename OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/{ExtensiveReadWritePathAnalysis.scala => ReadWritePathAnalysisPart.scala} (76%) delete mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala similarity index 76% rename from OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala rename to OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala index c0eb2e195c..c7e088b1ed 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ExtensiveReadWritePathAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala @@ -10,9 +10,6 @@ import org.opalj.br.fpcf.properties.Context import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.NonAssignable -import org.opalj.tac.SelfReferenceParameter -import org.opalj.tac.TACMethodParameter -import org.opalj.tac.TACode import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** @@ -25,36 +22,22 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites * * @author Maximilian Rüsch */ -trait ExtensiveReadWritePathAnalysis private[fieldassignability] +sealed trait ReadWritePathAnalysisPart private[fieldassignability] extends FieldAssignabilityAnalysisPart { /** - * Analyzing read-write paths interprocedurally is not fully supported yet. Instead, this identifies particular - * cases where read-write paths are provably impossible, given the information present in the framework / TAC. + * As a pre-stage to a full interprocedural read-write path analysis, users of this trait use this extension to + * specify whether it is provable that no execution path exists from the read path to the write path. * - * IMPROVE: The analysis of these accesses does not change over time, thus cache this derivation in the state + * IMPROVE: Results to not currently change over time, thus cache this derivation in the state * * @note Assumes that the two contexts point to different methods, i.e. that intraprocedural path existence is * handled separately. */ - private def canPathExistFromInitializerReadsToInitializerWrites( + protected def provablyNoPathExistsFromInitializerReadsToInitializerWrites( readContext: Context, writeContext: Context - )(implicit state: AnalysisState): Boolean = { - if (state.field.isStatic) { - // Writes to static fields in instance constructors are forbidden, see the write analysis. - - // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before - // the reading method, which is in turn only guaranteed when the writing static initializer is in the same - // type as the field declaration, i.e. it is run when the field is used for the first time. - - // Note that this also implies that there may be no supertype static initializer that can read the field, - // preventing read-write paths in the inheritance chain. - writeContext.method.declaringClassType ne state.field.classFile.thisType - } else { - true - } - } + )(implicit state: AnalysisState): Boolean override def completePatternWithInitializerRead( context: Context, @@ -70,7 +53,7 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] if (state.initializerWrites.exists { case (writeContext, _) => (writeContext.method ne context.method) && - canPathExistFromInitializerReadsToInitializerWrites(context, writeContext) + !provablyNoPathExistsFromInitializerReadsToInitializerWrites(context, writeContext) } ) return Some(Assignable); @@ -119,7 +102,7 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] if (state.initializerReads.exists { case (readContext, _) => (readContext.method ne context.method) && - canPathExistFromInitializerReadsToInitializerWrites(readContext, context) + !provablyNoPathExistsFromInitializerReadsToInitializerWrites(readContext, context) } ) return Some(Assignable); @@ -147,3 +130,43 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] receiver: Option[V] )(implicit state: AnalysisState): Option[FieldAssignability] = None } + +/** + * @inheritdoc + * + * The simple path analysis considers every interprocedural path to be harmful, causing the field to be assignable. + */ +trait SimpleReadWritePathAnalysis private[fieldassignability] extends ReadWritePathAnalysisPart { + + override protected final def provablyNoPathExistsFromInitializerReadsToInitializerWrites( + readContext: Context, + writeContext: Context + )(implicit state: AnalysisState): Boolean = false +} + +/** + * @inheritdoc + * + * The extensive path analysis considers interprocedural static field writes as safe when the writing static initializer + * is in the same class as the field declaration. + */ +trait ExtensiveReadWritePathAnalysis private[fieldassignability] + extends ReadWritePathAnalysisPart { + + override protected final def provablyNoPathExistsFromInitializerReadsToInitializerWrites( + readContext: Context, + writeContext: Context + )(implicit state: AnalysisState): Boolean = { + if (state.field.isStatic && writeContext.method.definedMethod.isStaticInitializer) { + // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before + // the reading method, which is in turn only guaranteed when the writing static initializer is in the same + // type as the field declaration, i.e. it is run when the field is used for the first time. + + // Note that this also implies that there may be no supertype static initializer that can read the field, + // preventing read-write paths in the inheritance chain. + writeContext.method.declaringClassType eq state.field.classFile.thisType + } else { + false + } + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala deleted file mode 100644 index f81ab8325b..0000000000 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/SimpleReadWritePathAnalysis.scala +++ /dev/null @@ -1,106 +0,0 @@ -/* BSD 2-Clause License - see OPAL/LICENSE for details. */ -package org.opalj -package tac -package fpcf -package analyses -package fieldassignability - -import org.opalj.br.PC -import org.opalj.br.fpcf.properties.Context -import org.opalj.br.fpcf.properties.immutability.Assignable -import org.opalj.br.fpcf.properties.immutability.FieldAssignability -import org.opalj.br.fpcf.properties.immutability.NonAssignable -import org.opalj.tac.SelfReferenceParameter -import org.opalj.tac.TACMethodParameter -import org.opalj.tac.TACode - -/** - * Determines the assignability of a field based on whether it is possible for a path to exist from a read to a write - * of the same field on the same instance. - * - * This part will always return a value, and will soundly abort if it cannot guarantee that no such path exists. This - * may be the case e.g. if a path from some read of the field to the write exists, but the analysis cannot prove that - * the two instances are disjoint. - * - * @author Maximilian Rüsch - */ -trait SimpleReadWritePathAnalysis private[fieldassignability] - extends FieldAssignabilityAnalysisPart { - - override def completePatternWithInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Some[FieldAssignability] = { - // Initializer reads are incompatible with arbitrary writes in other methods - if (state.nonInitializerWrites.nonEmpty) - return Some(Assignable); - - // Analyzing read-write paths interprocedurally is not supported yet. For static fields, initializer writes are - // harmless (i.e. executed before) when the read is in a constructor, so the class is already initialized. - if (state.initializerWrites.exists(_._1.method ne context.method)) - return Some(Assignable); - - val pathFromReadToSomeWriteExists = state.initializerWrites(context).exists { - case (writePC, _) => - pathExists(readPC, writePC, tac) - } - - if (pathFromReadToSomeWriteExists) - Some(Assignable) - else - Some(NonAssignable) - } - - override def completePatternWithNonInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None - - override def completePatternWithInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Some[FieldAssignability] = { - val method = context.method.definedMethod - if (state.field.isStatic && method.isConstructor || state.field.isNotStatic && method.isStaticInitializer) - return Some(Assignable); - - // For instance writes, there might be premature value escapes through arbitrary use sites. - // If we cannot guarantee that all use sites of the written instance are safe, we must soundly abort. - if (state.field.isNotStatic) { - // We only support modifying fields in initializers on newly created instances (which is always "this") - if (receiver.isEmpty || receiver.get.definedBy != SelfReferenceParameter) - return Some(Assignable); - - if (!tac.params.thisParameter.useSites.forall(isWrittenInstanceUseSiteSafe(tac, writePC, _))) - return Some(Assignable); - } - - // Analyzing read-write paths interprocedurally is not supported yet. However, for static fields, all - // reads that take place in instance constructors are harmless. - if (state.initializerReads.exists(_._1.method ne context.method)) - return Some(Assignable); - - val pathFromSomeReadToWriteExists = state.initializerReads(context).exists { - case (readPC, _) => - pathExists(readPC, writePC, tac) - } - - if (pathFromSomeReadToWriteExists) - Some(Assignable) - else - Some(NonAssignable) - } - - override def completePatternWithNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None -} From 03099b3e59f687a37dc7f9fe669eff14dfdeca43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Fri, 14 Nov 2025 18:06:27 +0100 Subject: [PATCH 25/41] Add class constant lazy init tests --- .../GeneratedClassConstantLazyInit.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/pre_bc_52_class_constant/GeneratedClassConstantLazyInit.java diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/pre_bc_52_class_constant/GeneratedClassConstantLazyInit.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/pre_bc_52_class_constant/GeneratedClassConstantLazyInit.java new file mode 100644 index 0000000000..a598a10be7 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/pre_bc_52_class_constant/GeneratedClassConstantLazyInit.java @@ -0,0 +1,65 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.lazyinitialization.pre_bc_52_class_constant; + +import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; +import org.opalj.fpcf.properties.immutability.field_assignability.LazilyInitializedField; +import org.opalj.fpcf.properties.immutability.field_assignability.UnsafelyLazilyInitializedField; +import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; + +/** + * Contains lazy initializations of class constants as produced by compilers before class literal support was officially + * introduced, i.e. before Java 5 (Bytecode version 49.0). Before class literals became part of the constant pool, they + * used a synthetic singleton pattern, with inlined lazy initialization at the usage site. + */ +public class GeneratedClassConstantLazyInit { + + private class ClassA {} + + static Class class$(String className) { + try { + return (Class) Class.forName(className); + } catch (ClassNotFoundException e) { + return null; + } + } + + @LazilyInitializedField(value = "A well behaved class loader returns singleton class instances (see JVM spec 5.3), and static initializers are inherently thread safe", + analyses = {}) + @AssignableField(value = "The analysis only recognizes lazy init patterns in instance methods", + analyses = { L2FieldAssignabilityAnalysis.class }) + private static Class class$lazyInitInStaticInitializer; + + static { + Class instance = class$lazyInitInStaticInitializer == null + ? (class$lazyInitInStaticInitializer = class$("string1")) + : class$lazyInitInStaticInitializer; + } + + @LazilyInitializedField(value = "A well behaved class loader returns singleton class instances (see JVM spec 5.3)", + analyses = {}) + @UnsafelyLazilyInitializedField(value = "The analysis does currently not incorporate singleton information", + analyses = { L2FieldAssignabilityAnalysis.class }) + private static Class class$lazyInitInInstanceMethod; + + void iUseClassConstantsOnce() { + Class instance = class$lazyInitInInstanceMethod == null + ? (class$lazyInitInInstanceMethod = class$("string2")) + : class$lazyInitInInstanceMethod; + } + + @LazilyInitializedField(value = "A well behaved class loader returns singleton class instances (see JVM spec 5.3)", + analyses = {}) + @AssignableField(value = "Multiple lazy initialization patterns for the same field are not supported", + analyses = { L2FieldAssignabilityAnalysis.class }) + private static Class class$multiPlaceLazyInit; + + void iUseClassConstantsTwice() { + Class instance = class$multiPlaceLazyInit == null + ? (class$multiPlaceLazyInit = class$("string3")) + : class$multiPlaceLazyInit; + + Class other = class$multiPlaceLazyInit == null + ? (class$multiPlaceLazyInit = class$("string4")) + : class$multiPlaceLazyInit; + } +} From 1a5755aed19a0c8c3c6122aeb0ed531c04a6dc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Fri, 14 Nov 2025 19:15:54 +0100 Subject: [PATCH 26/41] Add test for multiple writes --- .../EffectivelyNonAssignable.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/EffectivelyNonAssignable.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/EffectivelyNonAssignable.java index 137b7c65a2..535372fb53 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/EffectivelyNonAssignable.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/EffectivelyNonAssignable.java @@ -85,3 +85,21 @@ public void callNopOfClassWithMutableFields(){ @EffectivelyNonAssignableField("The field is not written after initialization") private HashMap effectivelyNonAssignableHashMap = new HashMap(); } + +class MultiWritesInConstructor { + @EffectivelyNonAssignableField("The field is only observed with one value") + private boolean ignoreAutoboxing; + + public MultiWritesInConstructor() { + this(false); + } + + public MultiWritesInConstructor(boolean ignoreAutoboxing) { + this.ignoreAutoboxing = false; + this.ignoreAutoboxing = ignoreAutoboxing; + } + + public boolean isIgnoringAutoboxing() { + return this.ignoreAutoboxing; + } +} From d6ff13f847fb02f6f427dc3962e4bf9818968885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 14:06:25 +0100 Subject: [PATCH 27/41] Clarify read write path analysis --- .../ReadWritePathAnalysisPart.scala | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala index c7e088b1ed..f6672e180e 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala @@ -14,7 +14,7 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** * Determines the assignability of a field based on whether it is possible for a path to exist from a read to a write - * of the same field on the same instance. + * of the same field on the same instance (or class for static fields). * * This part will always return a value, and will soundly abort if it cannot guarantee that no such path exists. This * may be the case e.g. if a path from some read of the field to the write exists, but the analysis cannot prove that @@ -26,15 +26,13 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] extends FieldAssignabilityAnalysisPart { /** - * As a pre-stage to a full interprocedural read-write path analysis, users of this trait use this extension to - * specify whether it is provable that no execution path exists from the read path to the write path. - * - * IMPROVE: Results to not currently change over time, thus cache this derivation in the state + * Allows users of this trait to specify whether the write context is provably unreachable from the read context. + * Implementations must be sound, and abort with 'false' if the result cannot be determined at the current time. * * @note Assumes that the two contexts point to different methods, i.e. that intraprocedural path existence is * handled separately. */ - protected def provablyNoPathExistsFromInitializerReadsToInitializerWrites( + protected def isContextUnreachableFrom( readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean @@ -53,7 +51,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] if (state.initializerWrites.exists { case (writeContext, _) => (writeContext.method ne context.method) && - !provablyNoPathExistsFromInitializerReadsToInitializerWrites(context, writeContext) + !isContextUnreachableFrom(context, writeContext) } ) return Some(Assignable); @@ -102,7 +100,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] if (state.initializerReads.exists { case (readContext, _) => (readContext.method ne context.method) && - !provablyNoPathExistsFromInitializerReadsToInitializerWrites(readContext, context) + !isContextUnreachableFrom(readContext, context) } ) return Some(Assignable); @@ -138,7 +136,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] */ trait SimpleReadWritePathAnalysis private[fieldassignability] extends ReadWritePathAnalysisPart { - override protected final def provablyNoPathExistsFromInitializerReadsToInitializerWrites( + override protected final def isContextUnreachableFrom( readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean = false @@ -153,11 +151,12 @@ trait SimpleReadWritePathAnalysis private[fieldassignability] extends ReadWriteP trait ExtensiveReadWritePathAnalysis private[fieldassignability] extends ReadWritePathAnalysisPart { - override protected final def provablyNoPathExistsFromInitializerReadsToInitializerWrites( + override protected final def isContextUnreachableFrom( readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean = { - if (state.field.isStatic && writeContext.method.definedMethod.isStaticInitializer) { + val writeMethod = writeContext.method.definedMethod + if (state.field.isStatic && writeMethod.isStaticInitializer) { // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before // the reading method, which is in turn only guaranteed when the writing static initializer is in the same // type as the field declaration, i.e. it is run when the field is used for the first time. From eeb2bec7a5ecf79d84686becd86a1e101d2244c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 15:46:20 +0100 Subject: [PATCH 28/41] Rework the test framework --- .../clone_function/SimpleClonePattern.java | 22 ++++--- .../DifferentLazyInitializedFieldTypes.java | 4 +- .../field_assignability/AssignableField.java | 26 ++++----- .../EffectivelyNonAssignableField.java | 20 ++++--- .../LazilyInitializedField.java | 16 ++--- .../NonAssignableField.java | 22 ++++--- .../UnsafelyLazilyInitializedField.java | 16 ++--- .../opalj/fpcf/FieldAssignabilityTests.scala | 58 +++++++++---------- 8 files changed, 104 insertions(+), 80 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/clone_function/SimpleClonePattern.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/clone_function/SimpleClonePattern.java index cf68de1262..60d0fb7f7a 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/clone_function/SimpleClonePattern.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/clone_function/SimpleClonePattern.java @@ -6,6 +6,7 @@ import org.opalj.fpcf.properties.immutability.fields.TransitivelyImmutableField; import org.opalj.fpcf.properties.immutability.field_assignability.EffectivelyNonAssignableField; import org.opalj.fpcf.properties.immutability.types.TransitivelyImmutableType; +import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; /** * This class encompasses different possible cases of the clone pattern. @@ -15,7 +16,8 @@ public final class SimpleClonePattern { @TransitivelyImmutableField("Field is effectively non assignable and has a primitive type") - @EffectivelyNonAssignableField("Field is only assigned ones due to the clone function pattern") + @EffectivelyNonAssignableField(value = "Field is only assigned once due to the clone function pattern", + analyses = { L2FieldAssignabilityAnalysis.class }) private int i; public SimpleClonePattern clone(){ @@ -28,7 +30,8 @@ public SimpleClonePattern clone(){ class CloneNonAssignableWithNewObject { @TransitivelyImmutableField("field is effectively non assignable and assigned with a transitively immutable object") - @EffectivelyNonAssignableField("field is only assigned ones due to the clone function pattern") + @EffectivelyNonAssignableField(value = "field is only assigned once due to the clone function pattern", + analyses = { L2FieldAssignabilityAnalysis.class }) private Integer integer; public CloneNonAssignableWithNewObject clone(){ @@ -41,7 +44,8 @@ public CloneNonAssignableWithNewObject clone(){ class EscapesAfterAssignment { @TransitivelyImmutableField("field is effectively non assignable and assigned with a transitively immutable object") - @EffectivelyNonAssignableField("field is only assigned ones due to the clone function pattern") + @EffectivelyNonAssignableField(value = "field is only assigned once due to the clone function pattern", + analyses = { L2FieldAssignabilityAnalysis.class }) private Integer integer; private Integer integerCopy; @@ -59,11 +63,13 @@ public EscapesAfterAssignment clone(){ final class MultipleFieldsAssignedInCloneFunction { @TransitivelyImmutableField("The field is effectively non assignable and has a transitively immutable type") - @EffectivelyNonAssignableField("The field is only assigned once in the clone function") + @EffectivelyNonAssignableField(value = "The field is only assigned once in the clone function", + analyses = { L2FieldAssignabilityAnalysis.class }) private Integer firstInteger; @TransitivelyImmutableField("The field is effectively non assignable and has a transitively immutable type") - @EffectivelyNonAssignableField("The field is only assigned once in the clone function") + @EffectivelyNonAssignableField(value = "The field is only assigned once in the clone function", + analyses = { L2FieldAssignabilityAnalysis.class }) private Integer secondInteger; public MultipleFieldsAssignedInCloneFunction clone(){ @@ -77,7 +83,8 @@ public MultipleFieldsAssignedInCloneFunction clone(){ class ConstructorWithParameter { @TransitivelyImmutableField("field is effectively non assignable and has a transitively immutable type") - @EffectivelyNonAssignableField("field is only assigned ones due to the clone function pattern") + @EffectivelyNonAssignableField(value = "field is only assigned once due to the clone function pattern", + analyses = { L2FieldAssignabilityAnalysis.class }) private Integer integer; public ConstructorWithParameter(Integer integer){ @@ -94,7 +101,8 @@ public ConstructorWithParameter clone(Integer integer){ class CloneNonAssignableArrayWithRead { - @EffectivelyNonAssignableField("field is only assigned once due to the clone function pattern") + @EffectivelyNonAssignableField(value = "field is only assigned once due to the clone function pattern", + analyses = { L2FieldAssignabilityAnalysis.class }) private boolean[] booleans; public CloneNonAssignableArrayWithRead clone(){ diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/objects/DifferentLazyInitializedFieldTypes.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/objects/DifferentLazyInitializedFieldTypes.java index cc1f52e792..9891f08b47 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/objects/DifferentLazyInitializedFieldTypes.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/objects/DifferentLazyInitializedFieldTypes.java @@ -16,8 +16,8 @@ */ public class DifferentLazyInitializedFieldTypes { - @TransitivelyImmutableField("Lazy initialized field with primitive type.") - @LazilyInitializedField("field is thread safely lazy initialized") + @TransitivelyImmutableField("Lazy initialized field with primitive type.") + @LazilyInitializedField("field is thread safely lazy initialized") private int inTheGetterLazyInitializedIntField; public synchronized int getInTheGetterLazyInitializedIntField(){ diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/AssignableField.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/AssignableField.java index f16292acba..205f2b4bc8 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/AssignableField.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/AssignableField.java @@ -1,12 +1,12 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.properties.immutability.field_assignability; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.*; import org.opalj.br.fpcf.FPCFAnalysis; import org.opalj.fpcf.properties.PropertyValidator; +import org.opalj.tac.fpcf.analyses.fieldassignability.L0FieldAssignabilityAnalysis; +import org.opalj.tac.fpcf.analyses.fieldassignability.L1FieldAssignabilityAnalysis; import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; /** @@ -14,22 +14,22 @@ * * @author Tobias Peter Roth */ -@PropertyValidator(key = "FieldAssignability",validator = AssignableFieldMatcher.class) +@PropertyValidator(key = "FieldAssignability", validator = AssignableFieldMatcher.class) @Documented @Retention(RetentionPolicy.CLASS) +@Target({ ElementType.FIELD }) public @interface AssignableField { - - /** - * True if the field is non-final because it is read prematurely. - * Tests may ignore @NonFinal annotations if the FieldPrematurelyRead property for the field - * did not identify the premature read. - */ - boolean prematurelyRead() default false; /** * A short reasoning of this property. */ String value() default "N/A"; - Class[] analyses() default {L2FieldAssignabilityAnalysis.class}; - + /** + * Which analyses should recognize this annotation instance. + */ + Class[] analyses() default { + L0FieldAssignabilityAnalysis.class, + L1FieldAssignabilityAnalysis.class, + L2FieldAssignabilityAnalysis.class, + }; } diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/EffectivelyNonAssignableField.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/EffectivelyNonAssignableField.java index fc9117d488..8cccc7bf0f 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/EffectivelyNonAssignableField.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/EffectivelyNonAssignableField.java @@ -1,12 +1,12 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.properties.immutability.field_assignability; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.*; import org.opalj.br.fpcf.FPCFAnalysis; import org.opalj.fpcf.properties.PropertyValidator; +import org.opalj.tac.fpcf.analyses.fieldassignability.L0FieldAssignabilityAnalysis; +import org.opalj.tac.fpcf.analyses.fieldassignability.L1FieldAssignabilityAnalysis; import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; /** @@ -14,16 +14,22 @@ * * @author Tobias Peter Roth */ -@PropertyValidator(key = "FieldAssignability",validator = EffectivelyNonAssignableFieldMatcher.class) +@PropertyValidator(key = "FieldAssignability", validator = EffectivelyNonAssignableFieldMatcher.class) @Documented @Retention(RetentionPolicy.CLASS) +@Target({ ElementType.FIELD }) public @interface EffectivelyNonAssignableField { - /** * A short reasoning of this property. */ String value(); - Class[] analyses() default {L2FieldAssignabilityAnalysis.class}; - + /** + * Which analyses should recognize this annotation instance. + */ + Class[] analyses() default { + L0FieldAssignabilityAnalysis.class, + L1FieldAssignabilityAnalysis.class, + L2FieldAssignabilityAnalysis.class, + }; } diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/LazilyInitializedField.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/LazilyInitializedField.java index c4b490977f..607110d90e 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/LazilyInitializedField.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/LazilyInitializedField.java @@ -1,9 +1,7 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.properties.immutability.field_assignability; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.*; import org.opalj.br.fpcf.FPCFAnalysis; import org.opalj.fpcf.properties.PropertyValidator; @@ -14,16 +12,20 @@ * * @author Tobias Peter Roth */ -@PropertyValidator(key = "FieldAssignability",validator = LazilyInitializedFieldMatcher.class) +@PropertyValidator(key = "FieldAssignability", validator = LazilyInitializedFieldMatcher.class) @Documented @Retention(RetentionPolicy.CLASS) +@Target({ ElementType.FIELD }) public @interface LazilyInitializedField { - /** * A short reasoning of this property. */ String value(); - Class[] analyses() default {L2FieldAssignabilityAnalysis.class}; - + /** + * Which analyses should recognize this annotation instance. + */ + Class[] analyses() default { + L2FieldAssignabilityAnalysis.class, + }; } diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/NonAssignableField.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/NonAssignableField.java index f45f87eaea..5a2de2b9aa 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/NonAssignableField.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/NonAssignableField.java @@ -1,29 +1,35 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.properties.immutability.field_assignability; +import java.lang.annotation.*; + import org.opalj.br.fpcf.FPCFAnalysis; import org.opalj.fpcf.properties.PropertyValidator; +import org.opalj.tac.fpcf.analyses.fieldassignability.L0FieldAssignabilityAnalysis; +import org.opalj.tac.fpcf.analyses.fieldassignability.L1FieldAssignabilityAnalysis; import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - /** * Annotation to state that the annotated field is final. * * @author Tobias Peter Roth */ -@PropertyValidator(key = "FieldAssignability",validator = NonAssignableFieldMatcher.class) +@PropertyValidator(key = "FieldAssignability", validator = NonAssignableFieldMatcher.class) @Documented @Retention(RetentionPolicy.CLASS) +@Target({ ElementType.FIELD}) public @interface NonAssignableField { - /** * A short reasoning of this property. */ String value(); - Class[] analyses() default {L2FieldAssignabilityAnalysis.class}; - + /** + * Which analyses should recognize this annotation instance. + */ + Class[] analyses() default { + L0FieldAssignabilityAnalysis.class, + L1FieldAssignabilityAnalysis.class, + L2FieldAssignabilityAnalysis.class, + }; } diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/UnsafelyLazilyInitializedField.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/UnsafelyLazilyInitializedField.java index c707367999..f2d5c9530d 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/UnsafelyLazilyInitializedField.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/properties/immutability/field_assignability/UnsafelyLazilyInitializedField.java @@ -1,9 +1,7 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.properties.immutability.field_assignability; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.*; import org.opalj.br.fpcf.FPCFAnalysis; import org.opalj.fpcf.properties.PropertyValidator; @@ -12,16 +10,20 @@ /** * Annotation to state that the annotated field is unsafely lazily initialized. */ -@PropertyValidator(key = "FieldAssignability",validator = UnsafelyLazilyInitializedFieldMatcher.class) +@PropertyValidator(key = "FieldAssignability", validator = UnsafelyLazilyInitializedFieldMatcher.class) @Documented @Retention(RetentionPolicy.CLASS) +@Target({ ElementType.FIELD }) public @interface UnsafelyLazilyInitializedField { - /** * A short reasoning of this property. */ String value(); - Class[] analyses() default {L2FieldAssignabilityAnalysis.class}; - + /** + * Which analyses should recognize this annotation instance. + */ + Class[] analyses() default { + L2FieldAssignabilityAnalysis.class, + }; } diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala index 14d14d30e1..ac5d2df519 100644 --- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala @@ -14,12 +14,15 @@ import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyReturnValueFreshnessAnalysis import org.opalj.tac.fpcf.analyses.fieldaccess.EagerFieldAccessInformationAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL0FieldAssignabilityAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL1FieldAssignabilityAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL2FieldAssignabilityAnalysis /** * Tests the field assignability analysis * * @author Tobias Roth + * @author Maximilian Rüsch */ class FieldAssignabilityTests extends PropertiesTest { @@ -46,38 +49,35 @@ class FieldAssignabilityTests extends PropertiesTest { validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) } -// describe("the org.opalj.fpcf.analyses.L0FieldAssignability is executed") { -// -// val as = executeAnalyses( -// Set( -// EagerL0FieldAssignabilityAnalysis, -// LazyInterProceduralEscapeAnalysis, -// LazyReturnValueFreshnessAnalysis, -// LazyFieldLocalityAnalysis, -// EagerFieldAccessInformationAnalysis -// ) -// ) -// as.propertyStore.shutdown() -// validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) -// } + describe("the org.opalj.fpcf.analyses.L0FieldAssignability is executed") { + val as = executeAnalyses( + Set( + EagerL0FieldAssignabilityAnalysis, + LazyInterProceduralEscapeAnalysis, + LazyReturnValueFreshnessAnalysis, + LazyFieldLocalityAnalysis, + EagerFieldAccessInformationAnalysis + ) + ) + as.propertyStore.shutdown() + validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + } -// describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executed") { -// -// val as = executeAnalyses( -// Set( -// EagerL1FieldAssignabilityAnalysis, -// LazyInterProceduralEscapeAnalysis, -// LazyReturnValueFreshnessAnalysis, -// LazyFieldLocalityAnalysis, -// EagerFieldAccessInformationAnalysis -// ) -// ) -// as.propertyStore.shutdown() -// validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) -// } + describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executed") { + val as = executeAnalyses( + Set( + EagerL1FieldAssignabilityAnalysis, + LazyInterProceduralEscapeAnalysis, + LazyReturnValueFreshnessAnalysis, + LazyFieldLocalityAnalysis, + EagerFieldAccessInformationAnalysis + ) + ) + as.propertyStore.shutdown() + validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + } describe("the org.opalj.fpcf.analyses.L2FieldAssignability is executed") { - val as = executeAnalyses( Set( EagerL2FieldAssignabilityAnalysis, From 177feae065e8d3287618224e1255ce43504a43d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 16:22:04 +0100 Subject: [PATCH 29/41] Check precision of tested values strictly increases --- .../opalj/fpcf/FieldAssignabilityTests.scala | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala index ac5d2df519..2b0b000c80 100644 --- a/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala +++ b/DEVELOPING_OPAL/validate/src/test/scala/org/opalj/fpcf/FieldAssignabilityTests.scala @@ -8,7 +8,10 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import org.opalj.ai.fpcf.properties.AIDomainFactoryKey +import org.opalj.br.Field import org.opalj.br.analyses.Project +import org.opalj.br.analyses.SomeProject +import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.tac.cg.RTACallGraphKey import org.opalj.tac.fpcf.analyses.LazyFieldLocalityAnalysis import org.opalj.tac.fpcf.analyses.escape.LazyInterProceduralEscapeAnalysis @@ -36,8 +39,8 @@ class FieldAssignabilityTests extends PropertiesTest { override def init(p: Project[URL]): Unit = { p.updateProjectInformationKeyInitializationData(AIDomainFactoryKey) { _ => - import org.opalj.ai.domain.l1 - Set[Class[? <: AnyRef]](classOf[l1.DefaultDomainWithCFGAndDefUse[URL]]) + import org.opalj.ai.domain + Set[Class[? <: AnyRef]](classOf[domain.l1.DefaultDomainWithCFGAndDefUse[URL]]) } p.get(RTACallGraphKey) @@ -49,7 +52,8 @@ class FieldAssignabilityTests extends PropertiesTest { validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) } - describe("the org.opalj.fpcf.analyses.L0FieldAssignability is executed") { + var l0: Option[(SomeProject, PropertyStore)] = None + describe("the org.opalj.fpcf.analyses.L0FieldAssignability is executable") { val as = executeAnalyses( Set( EagerL0FieldAssignabilityAnalysis, @@ -60,10 +64,15 @@ class FieldAssignabilityTests extends PropertiesTest { ) ) as.propertyStore.shutdown() - validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + l0 = Some(as.project, as.propertyStore) + + describe("and produces the correct properties") { + validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + } } - describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executed") { + var l1: Option[(SomeProject, PropertyStore)] = None + describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executable") { val as = executeAnalyses( Set( EagerL1FieldAssignabilityAnalysis, @@ -74,10 +83,18 @@ class FieldAssignabilityTests extends PropertiesTest { ) ) as.propertyStore.shutdown() - validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + l1 = Some(as.project, as.propertyStore) + + describe("and produces the correct properties") { + validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + } + + it("and produces properties at least as precise as L0") { + checkMorePrecise(l0.get._1, l0.get._2, as.project, as.propertyStore) + } } - describe("the org.opalj.fpcf.analyses.L2FieldAssignability is executed") { + describe("the org.opalj.fpcf.analyses.L2FieldAssignability is executable") { val as = executeAnalyses( Set( EagerL2FieldAssignabilityAnalysis, @@ -88,6 +105,38 @@ class FieldAssignabilityTests extends PropertiesTest { ) ) as.propertyStore.shutdown() - validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + + describe("and produces the correct properties") { + validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + } + + it("and produces properties at least as precise as L1") { + checkMorePrecise(l1.get._1, l1.get._2, as.project, as.propertyStore) + } + } + + /** + * Represents a case where the alleged less precise value is not actually less (or equally) precise as the other. + */ + case class ImpreciseFieldValue(field: Field, lessPrecise: FieldAssignability, morePrecise: FieldAssignability) + + private def checkMorePrecise( + lessPreciseProject: SomeProject, + lessPrecisePS: PropertyStore, + morePreciseProject: SomeProject, + morePrecisePS: PropertyStore + ): Unit = { + var impreciseFieldValues: List[ImpreciseFieldValue] = List.empty + morePreciseProject.allFields.foreach { field => + val lessPreciseValue = lessPrecisePS.get(field, FieldAssignability.key).get.asFinal.p + val morePreciseValue = morePrecisePS.get(field, FieldAssignability.key).get.asFinal.p + + // The less precise value should be "strictly" less precise, meaning it is ordered w.r.t. the precise value + if (lessPreciseValue.meet(morePreciseValue) ne lessPreciseValue) { + impreciseFieldValues ::= ImpreciseFieldValue(field, lessPreciseValue, morePreciseValue) + } + } + + assert(impreciseFieldValues.isEmpty, s"found precision violations:\n${impreciseFieldValues.mkString("\n")}") } } From f5dbfe4ae1a827cc0bce142fa40859b9de5f2eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 16:22:14 +0100 Subject: [PATCH 30/41] Remove unused property --- .../br/fpcf/properties/immutability/FieldAssignability.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala index 3d7c5857a8..9c1f6acee4 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/properties/immutability/FieldAssignability.scala @@ -40,8 +40,6 @@ sealed trait FieldAssignability extends OrderedProperty with FieldAssignabilityP object FieldAssignability extends FieldAssignabilityPropertyMetaInformation { - var notEscapes: Boolean = false - final val PropertyKeyName = "opalj.FieldAssignability" final val key: PropertyKey[FieldAssignability] = { From 0d78235c3c7498d23d44be698307ad42269046c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 16:26:14 +0100 Subject: [PATCH 31/41] Remove L0 from lazy init tests --- .../lazyinitialization/scala_lazy_val/LazyCell.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java index c67d58f36e..918d65ca51 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/lazyinitialization/scala_lazy_val/LazyCell.java @@ -1,7 +1,6 @@ /* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj.fpcf.fixtures.immutability.openworld.lazyinitialization.scala_lazy_val; -import org.opalj.tac.fpcf.analyses.fieldassignability.L0FieldAssignabilityAnalysis; import org.opalj.fpcf.properties.immutability.classes.TransitivelyImmutableClass; import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.LazilyInitializedField; @@ -19,13 +18,13 @@ public class LazyCell { @TransitivelyImmutableField(value = "The field is lazily initialized and has a primitive type", analyses = {}) @LazilyInitializedField(value = "The field is only set once in a synchronized way.", analyses = {}) @AssignableField(value = "The analyses cannot recognize lazy initialization over multiple methods", - analyses = { L0FieldAssignabilityAnalysis.class, L2FieldAssignabilityAnalysis.class }) + analyses = { L2FieldAssignabilityAnalysis.class }) private volatile boolean bitmap_0 = false; @TransitivelyImmutableField(value = "The field is lazily initialized and has a primitive type", analyses = {}) @LazilyInitializedField(value = "The field is only set once in a synchronized way.", analyses = {}) @AssignableField(value = "The analysis cannot recognize lazy initialization over multiple methods", - analyses = { L0FieldAssignabilityAnalysis.class, L2FieldAssignabilityAnalysis.class }) + analyses = { L2FieldAssignabilityAnalysis.class }) Integer value_0; private Integer value_lazy_compute() { From 8a83fa44e8bb143e754ae2c07c5334a5a4737623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 20:29:18 +0100 Subject: [PATCH 32/41] Refactor analysis part framework to be inheritance based --- .../AbstractFieldAssignabilityAnalysis.scala | 53 ++++++++----------- .../L0FieldAssignabilityAnalysis.scala | 12 ++--- .../L1FieldAssignabilityAnalysis.scala | 13 +---- .../L2FieldAssignabilityAnalysis.scala | 25 +++------ .../{ => part}/ClonePatternAnalysis.scala | 22 +++----- .../FieldAssignabilityAnalysisPart.scala | 40 +++----------- .../LazyInitializationAnalysis.scala | 37 ++++--------- .../part/PartAnalysisAbstractions.scala | 38 +++++++++++++ .../ReadWritePathAnalysisPart.scala | 24 +++------ 9 files changed, 103 insertions(+), 161 deletions(-) rename OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/{ => part}/ClonePatternAnalysis.scala (84%) rename OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/{ => part}/FieldAssignabilityAnalysisPart.scala (75%) rename OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/{ => part}/LazyInitializationAnalysis.scala (97%) create mode 100644 OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala rename OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/{ => part}/ReadWritePathAnalysisPart.scala (91%) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala index ee00dc5144..7f75ed21fa 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/AbstractFieldAssignabilityAnalysis.scala @@ -44,6 +44,7 @@ import org.opalj.fpcf.SomeEPS import org.opalj.fpcf.UBP import org.opalj.tac.common.DefinitionSitesKey import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites +import org.opalj.tac.fpcf.analyses.fieldassignability.part.PartAnalysisAbstractions import org.opalj.tac.fpcf.properties.TACAI trait AbstractFieldAssignabilityAnalysisState { @@ -76,28 +77,32 @@ trait AbstractFieldAssignabilityAnalysisState { /** * Base trait for all field assignability analyses. Analyses are comprised of a sequence of - * [[FieldAssignabilityAnalysisPart]], which are provided one-by-one with accesses of the field under analysis. + * [[part.FieldAssignabilityAnalysisPart]], which are provided one-by-one with accesses of the field under analysis. * * @note Analysis derived from this trait are only ''soundy'' if the project does not contain native methods, as long as * the [[FieldReadAccessInformation]] and [[FieldWriteAccessInformation]] do not recognize such accesses. * - * @see [[FieldAssignabilityAnalysisPart]] + * @see [[part.FieldAssignabilityAnalysisPart]] * * @author Maximilian Rüsch * @author Dominik Helm */ -trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { +trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis with PartAnalysisAbstractions { - protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] + private var parts: Seq[PartInfo] = Seq.empty + + override def registerPart(partInfo: PartInfo): Unit = { + parts = parts :+ partInfo + } private def determineAssignabilityFromParts( - partFunc: FieldAssignabilityAnalysisPart => Option[FieldAssignability] + hookFunc: PartInfo => Option[FieldAssignability] ): Option[FieldAssignability] = { var assignability: Option[FieldAssignability] = None for { - part <- parts + partInfo <- parts if !assignability.contains(Assignable) - partAssignability = partFunc(part) + partAssignability = hookFunc(partInfo) if partAssignability.isDefined } { if (assignability.isDefined) @@ -126,7 +131,7 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { } } - type AnalysisState <: AbstractFieldAssignabilityAnalysisState + override type AnalysisState <: AbstractFieldAssignabilityAnalysisState def createState(field: Field): AnalysisState @@ -158,12 +163,8 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { readPC: PC, receiver: Option[V] )(implicit state: AnalysisState): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithInitializerRead(context, tac, readPC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(NonAssignable) + val result = determineAssignabilityFromParts(_.onInitializerRead(context, tac, readPC, receiver, state)) + result.getOrElse(NonAssignable) } private def analyzeNonInitializerRead( @@ -172,12 +173,8 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { readPC: PC, receiver: Option[V] )(implicit state: AnalysisState): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithNonInitializerRead(context, tac, readPC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(NonAssignable) + val result = determineAssignabilityFromParts(_.onNonInitializerRead(context, tac, readPC, receiver, state)) + result.getOrElse(NonAssignable) } private def analyzeInitializerWrite( @@ -186,12 +183,8 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { writePC: PC, receiver: Option[V] )(implicit state: AnalysisState): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithInitializerWrite(context, tac, writePC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(Assignable) + val result = determineAssignabilityFromParts(_.onInitializerWrite(context, tac, writePC, receiver, state)) + result.getOrElse(Assignable) } private def analyzeNonInitializerWrite( @@ -200,12 +193,8 @@ trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { writePC: PC, receiver: Option[V] )(implicit state: AnalysisState): FieldAssignability = { - val assignability = determineAssignabilityFromParts(part => - part.completePatternWithNonInitializerWrite(context, tac, writePC, receiver)(using - state.asInstanceOf[part.AnalysisState] - ) - ) - assignability.getOrElse(Assignable) + val result = determineAssignabilityFromParts(_.onNonInitializerWrite(context, tac, writePC, receiver, state)) + result.getOrElse(Assignable) } def createResult()(implicit state: AnalysisState): ProperPropertyComputationResult = { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index b74b1f290b..3b64dd5e98 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -7,6 +7,7 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject +import org.opalj.tac.fpcf.analyses.fieldassignability.part.SimpleReadWritePathAnalysis /** * Determines the assignability of a field based on a simple analysis of read -> write paths. @@ -17,19 +18,12 @@ import org.opalj.br.analyses.SomeProject * @author Dominik Helm */ class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) - extends AbstractFieldAssignabilityAnalysis { + extends AbstractFieldAssignabilityAnalysis + with SimpleReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) - - private class L0ReadWritePathPart(val project: SomeProject) extends SimpleReadWritePathAnalysis { - override type AnalysisState = L0FieldAssignabilityAnalysis.this.AnalysisState - } - - override protected lazy val parts = Seq( - L0ReadWritePathPart(project) - ) } object EagerL0FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala index 19a0e94240..00777b5dac 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -7,7 +7,7 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject -import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis /** * Determines the assignability of a field based on a more complex analysis of read-write paths than @@ -20,20 +20,11 @@ import org.opalj.br.fpcf.FPCFAnalysis */ class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - with FPCFAnalysis { + with ExtensiveReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState - with LazyInitializationAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) - - private class L1ReadWritePathPart(val project: SomeProject) extends ExtensiveReadWritePathAnalysis { - override type AnalysisState = L1FieldAssignabilityAnalysis.this.AnalysisState - } - - override protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] = List( - L1ReadWritePathPart(project) - ) } object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 28db99390f..4b8d050324 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -7,7 +7,10 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject -import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ClonePatternAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.LazyInitializationAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.LazyInitializationAnalysisState /** * Determines the assignability of a field based on a more complex analysis of read-write paths than @@ -19,28 +22,14 @@ import org.opalj.br.fpcf.FPCFAnalysis */ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - with FPCFAnalysis { + with LazyInitializationAnalysis + with ClonePatternAnalysis + with ExtensiveReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState with LazyInitializationAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) - - private class L2LazyInitializationPart(val project: SomeProject) extends LazyInitializationAnalysis { - override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState - } - private class L2ClonePatternPart(val project: SomeProject) extends ClonePatternAnalysis { - override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState - } - private class L2ReadWritePathPart(val project: SomeProject) extends ExtensiveReadWritePathAnalysis { - override type AnalysisState = L2FieldAssignabilityAnalysis.this.AnalysisState - } - - override protected lazy val parts: Seq[FieldAssignabilityAnalysisPart] = List( - L2LazyInitializationPart(project), - L2ClonePatternPart(project), - L2ReadWritePathPart(project) - ) } object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala similarity index 84% rename from OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala rename to OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala index 365d3bd054..85d3afd513 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala @@ -4,6 +4,7 @@ package tac package fpcf package analyses package fieldassignability +package part import org.opalj.br.PC import org.opalj.br.fpcf.properties.Context @@ -21,14 +22,12 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites trait ClonePatternAnalysis private[fieldassignability] extends FieldAssignabilityAnalysisPart { - override def completePatternWithInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None + registerPart(PartInfo( + onNonInitializerRead = onNonInitializerRead(_, _, _, _)(using _), + onNonInitializerWrite = onNonInitializerWrite(_, _, _, _)(using _), + )) - override def completePatternWithNonInitializerRead( + private def onNonInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: Int, @@ -50,14 +49,7 @@ trait ClonePatternAnalysis private[fieldassignability] None } - override def completePatternWithInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None - - override def completePatternWithNonInitializerWrite( + private def onNonInitializerWrite( context: Context, tac: TACode[TACMethodParameter, V], writePC: PC, diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala similarity index 75% rename from OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala rename to OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala index a2900c1041..1ece2a99b8 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/FieldAssignabilityAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala @@ -4,47 +4,21 @@ package tac package fpcf package analyses package fieldassignability +package part -import org.opalj.br.PC import org.opalj.br.fpcf.FPCFAnalysis -import org.opalj.br.fpcf.properties.Context -import org.opalj.br.fpcf.properties.immutability.FieldAssignability /** + * A collection of common tools for field assignability analysis parts, as well as a convenience type bound for state. + * Implementors should call `registerPart` (see [[PartAnalysisAbstractions]] and for some example + * [[LazyInitializationAnalysis]]) to realise the part as a mixin for [[AbstractFieldAssignabilityAnalysis]] subclasses. + * * @author Maximilian Rüsch */ trait FieldAssignabilityAnalysisPart private[fieldassignability] - extends FPCFAnalysis { + extends FPCFAnalysis with PartAnalysisAbstractions { - type AnalysisState <: AbstractFieldAssignabilityAnalysisState - - def completePatternWithInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] - - def completePatternWithNonInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: Int, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] - - def completePatternWithInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] - - def completePatternWithNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] + override type AnalysisState <: AbstractFieldAssignabilityAnalysisState /** * Provided with two PCs, determines whether there exists a path from the first PC to the second PC in the context diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/LazyInitializationAnalysis.scala similarity index 97% rename from OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala rename to OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/LazyInitializationAnalysis.scala index 067453ae63..3ea72260d4 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/LazyInitializationAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/LazyInitializationAnalysis.scala @@ -4,6 +4,7 @@ package tac package fpcf package analyses package fieldassignability +package part import scala.annotation.switch @@ -34,23 +35,6 @@ import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.LazilyInitialized import org.opalj.br.fpcf.properties.immutability.UnsafelyLazilyInitialized import org.opalj.collection.immutable.IntTrieSet -import org.opalj.tac.CaughtException -import org.opalj.tac.ClassConst -import org.opalj.tac.Compare -import org.opalj.tac.Expr -import org.opalj.tac.FieldWriteAccessStmt -import org.opalj.tac.GetField -import org.opalj.tac.GetStatic -import org.opalj.tac.If -import org.opalj.tac.MonitorEnter -import org.opalj.tac.MonitorExit -import org.opalj.tac.PrimitiveTypecastExpr -import org.opalj.tac.SelfReferenceParameter -import org.opalj.tac.Stmt -import org.opalj.tac.TACMethodParameter -import org.opalj.tac.TACode -import org.opalj.tac.Throw -import org.opalj.tac.VirtualFunctionCall trait LazyInitializationAnalysisState extends AbstractFieldAssignabilityAnalysisState { var potentialLazyInit: Option[(Context, Int, Int, TACode[TACMethodParameter, V])] = None @@ -72,6 +56,12 @@ trait LazyInitializationAnalysis private[fieldassignability] override type AnalysisState <: LazyInitializationAnalysisState + registerPart(PartInfo( + onInitializerRead = onInitializerRead(_, _, _, _)(using _), + onNonInitializerRead = onNonInitializerRead(_, _, _, _)(using _), + onNonInitializerWrite = onNonInitializerWrite(_, _, _, _)(using _) + )) + val considerLazyInitialization: Boolean = project.config.getBoolean( "org.opalj.fpcf.analyses.L2FieldAssignabilityAnalysis.considerLazyInitialization" @@ -107,7 +97,7 @@ trait LazyInitializationAnalysis private[fieldassignability] bbPotentiallyDominator == bbPotentiallyDominated && potentiallyDominatorIndex < potentiallyDominatedIndex } - override def completePatternWithInitializerRead( + private def onInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: PC, @@ -115,7 +105,7 @@ trait LazyInitializationAnalysis private[fieldassignability] )(implicit state: AnalysisState): Option[FieldAssignability] = state.potentialLazyInit.map(_ => Assignable) - override def completePatternWithNonInitializerRead( + private def onNonInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: Int, @@ -139,14 +129,7 @@ trait LazyInitializationAnalysis private[fieldassignability] None } - override def completePatternWithInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None - - override def completePatternWithNonInitializerWrite( + private def onNonInitializerWrite( context: Context, tac: TACode[TACMethodParameter, V], writePC: PC, diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala new file mode 100644 index 0000000000..41ff28cc3f --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala @@ -0,0 +1,38 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability +package part + +import org.opalj.br.PC +import org.opalj.br.fpcf.FPCFAnalysis +import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.immutability.FieldAssignability +import org.opalj.fpcf.SomeEPS + +/** + * @author Maximilian Rüsch + */ +trait PartAnalysisAbstractions private[fieldassignability] + extends FPCFAnalysis { + + type AnalysisState <: AnyRef + + private[fieldassignability] type PartHook = + (Context, TACode[TACMethodParameter, V], PC, Option[V], AnalysisState) => Option[FieldAssignability] + + private[fieldassignability] type PartContinuation = + (SomeEPS, AnalysisState) => Option[FieldAssignability] + + private[fieldassignability] case class PartInfo( + onInitializerRead: PartHook = (_, _, _, _, _) => None, + onNonInitializerRead: PartHook = (_, _, _, _, _) => None, + onInitializerWrite: PartHook = (_, _, _, _, _) => None, + onNonInitializerWrite: PartHook = (_, _, _, _, _) => None, + continuation: PartContinuation = (_, _) => None, + ) + + def registerPart(partInfo: PartInfo): Unit +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala similarity index 91% rename from OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala rename to OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala index f6672e180e..0b1660289b 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/ReadWritePathAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala @@ -4,6 +4,7 @@ package tac package fpcf package analyses package fieldassignability +package part import org.opalj.br.PC import org.opalj.br.fpcf.properties.Context @@ -25,6 +26,11 @@ import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites sealed trait ReadWritePathAnalysisPart private[fieldassignability] extends FieldAssignabilityAnalysisPart { + registerPart(PartInfo( + onInitializerRead = onInitializerRead(_, _, _, _)(using _), + onInitializerWrite = onInitializerWrite(_, _, _, _)(using _), + )) + /** * Allows users of this trait to specify whether the write context is provably unreachable from the read context. * Implementations must be sound, and abort with 'false' if the result cannot be determined at the current time. @@ -37,7 +43,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] writeContext: Context )(implicit state: AnalysisState): Boolean - override def completePatternWithInitializerRead( + private def onInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], readPC: PC, @@ -68,14 +74,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] Some(NonAssignable) } - override def completePatternWithNonInitializerRead( - context: Context, - tac: TACode[TACMethodParameter, V], - readPC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None - - override def completePatternWithInitializerWrite( + private def onInitializerWrite( context: Context, tac: TACode[TACMethodParameter, V], writePC: PC, @@ -120,13 +119,6 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] else Some(NonAssignable) } - - override def completePatternWithNonInitializerWrite( - context: Context, - tac: TACode[TACMethodParameter, V], - writePC: PC, - receiver: Option[V] - )(implicit state: AnalysisState): Option[FieldAssignability] = None } /** From 1859d081cdcfc2ec070069ce87160f01bb0ddefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 20:37:29 +0100 Subject: [PATCH 33/41] Fix mixed up field assignability schedulers --- .../L1FieldAssignabilityAnalysis.scala | 10 +++++----- .../L2FieldAssignabilityAnalysis.scala | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala index 00777b5dac..2d2feb3a30 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -27,10 +27,10 @@ class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: Som override def createState(field: Field): AnalysisState = State(field) } -object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { - override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) +object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) } -object LazyL2FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { - override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) -} +object LazyL1FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) +} \ No newline at end of file diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 4b8d050324..74574abeb8 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -32,10 +32,10 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som override def createState(field: Field): AnalysisState = State(field) } -object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { - override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) +object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) } -object LazyL1FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { - override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) +object LazyL2FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) } From 214562a80e54747a26bbb232c6f81d3337808021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 20:52:04 +0100 Subject: [PATCH 34/41] Make L0 use the slightly more sophisticated read write path analysis part --- .../fieldassignability/L0FieldAssignabilityAnalysis.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala index 3b64dd5e98..9b374db4ba 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L0FieldAssignabilityAnalysis.scala @@ -7,7 +7,7 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject -import org.opalj.tac.fpcf.analyses.fieldassignability.part.SimpleReadWritePathAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis /** * Determines the assignability of a field based on a simple analysis of read -> write paths. @@ -19,7 +19,7 @@ import org.opalj.tac.fpcf.analyses.fieldassignability.part.SimpleReadWritePathAn */ class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - with SimpleReadWritePathAnalysis { + with ExtensiveReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState type AnalysisState = State From eaeb9e6c8bbcaaef3ac57ba130351218053cb36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 17 Nov 2025 20:52:40 +0100 Subject: [PATCH 35/41] Rename test --- ...cFinalFields.java => StaticFieldWithDefaultValue.java} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/{ClassWithPublicFinalFields.java => StaticFieldWithDefaultValue.java} (78%) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/StaticFieldWithDefaultValue.java similarity index 78% rename from DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java rename to DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/StaticFieldWithDefaultValue.java index 8e2b589d43..72d04b5756 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/ClassWithPublicFinalFields.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/StaticFieldWithDefaultValue.java @@ -8,13 +8,13 @@ @MutableType("The type is extensible") @TransitivelyImmutableClass("Class has no instance fields") -public class ClassWithPublicFinalFields { +public class StaticFieldWithDefaultValue { @NonTransitivelyImmutableField("Field is not assignable") - @NonAssignableField("The field is public, final, and its value is only set once since it is static") + @NonAssignableField("The field is public, final, and its value is only set once in the static initializer") public static final Object DEFAULT = new Object(); - public ClassWithPublicFinalFields() { - System.out.println(ClassWithPublicFinalFields.DEFAULT); + public StaticFieldWithDefaultValue() { + System.out.println(StaticFieldWithDefaultValue.DEFAULT); } } From d46ca5ba4bb225c8309bd25cbbbcc4c8bb473e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Tue, 18 Nov 2025 14:14:21 +0100 Subject: [PATCH 36/41] Fix formatting --- .../L1FieldAssignabilityAnalysis.scala | 2 +- .../fieldassignability/part/ClonePatternAnalysis.scala | 2 +- .../part/PartAnalysisAbstractions.scala | 10 +++++----- .../part/ReadWritePathAnalysisPart.scala | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala index 2d2feb3a30..74b92adbd1 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -33,4 +33,4 @@ object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignability object LazyL1FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) -} \ No newline at end of file +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala index 85d3afd513..f294c899e1 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala @@ -24,7 +24,7 @@ trait ClonePatternAnalysis private[fieldassignability] registerPart(PartInfo( onNonInitializerRead = onNonInitializerRead(_, _, _, _)(using _), - onNonInitializerWrite = onNonInitializerWrite(_, _, _, _)(using _), + onNonInitializerWrite = onNonInitializerWrite(_, _, _, _)(using _) )) private def onNonInitializerRead( diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala index 41ff28cc3f..ac47439f60 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/PartAnalysisAbstractions.scala @@ -27,11 +27,11 @@ trait PartAnalysisAbstractions private[fieldassignability] (SomeEPS, AnalysisState) => Option[FieldAssignability] private[fieldassignability] case class PartInfo( - onInitializerRead: PartHook = (_, _, _, _, _) => None, - onNonInitializerRead: PartHook = (_, _, _, _, _) => None, - onInitializerWrite: PartHook = (_, _, _, _, _) => None, - onNonInitializerWrite: PartHook = (_, _, _, _, _) => None, - continuation: PartContinuation = (_, _) => None, + onInitializerRead: PartHook = (_, _, _, _, _) => None, + onNonInitializerRead: PartHook = (_, _, _, _, _) => None, + onInitializerWrite: PartHook = (_, _, _, _, _) => None, + onNonInitializerWrite: PartHook = (_, _, _, _, _) => None, + continuation: PartContinuation = (_, _) => None ) def registerPart(partInfo: PartInfo): Unit diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala index 0b1660289b..f9a14527dc 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala @@ -28,7 +28,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] registerPart(PartInfo( onInitializerRead = onInitializerRead(_, _, _, _)(using _), - onInitializerWrite = onInitializerWrite(_, _, _, _)(using _), + onInitializerWrite = onInitializerWrite(_, _, _, _)(using _) )) /** From 35b1c89b817bbd70681b182e4ed0d6d76a7e856b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Tue, 18 Nov 2025 16:29:13 +0100 Subject: [PATCH 37/41] Support side by side constructors via call graphs --- .../stringelements/SimpleStringModel.java | 6 +- .../L1FieldAssignabilityAnalysis.scala | 10 +- .../L2FieldAssignabilityAnalysis.scala | 6 +- .../part/ReadWritePathAnalysisPart.scala | 135 +++++++++++++++--- 4 files changed, 134 insertions(+), 23 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java index ecf8543c51..e9b68c6850 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java @@ -8,6 +8,7 @@ import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.LazilyInitializedField; import org.opalj.tac.fpcf.analyses.FieldImmutabilityAnalysis; +import org.opalj.tac.fpcf.analyses.fieldassignability.L1FieldAssignabilityAnalysis; import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; /** @@ -19,9 +20,8 @@ public final class SimpleStringModel { @TransitivelyImmutableField(value = "The array values are not mutated after the assignment ", analyses = {}) @MutableField(value = "The analysis can not recognize transitive immutable arrays, and the field is assignable", analyses = { FieldImmutabilityAnalysis.class }) - @NonAssignableField(value = "The field is final", analyses = {}) - @AssignableField(value = "The field is written read and written in two different initializers", - analyses = { L2FieldAssignabilityAnalysis.class }) + @NonAssignableField(value = "The field is final and it is written in two different initializers that do not call each other", + analyses = { L1FieldAssignabilityAnalysis.class, L2FieldAssignabilityAnalysis.class }) private final char value[]; public char[] getValue() { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala index 74b92adbd1..5828cf81f0 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -7,7 +7,10 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject -import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis +import org.opalj.br.fpcf.properties.cg.Callees +import org.opalj.fpcf.PropertyBounds +import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysisState /** * Determines the assignability of a field based on a more complex analysis of read-write paths than @@ -20,14 +23,17 @@ import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePat */ class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - with ExtensiveReadWritePathAnalysis { + with CalleesBasedReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState + with CalleesBasedReadWritePathAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) } object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def uses: Set[PropertyBounds] = super.uses + PropertyBounds.ub(Callees) + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 74574abeb8..2f1686b3e6 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -7,8 +7,9 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject +import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysisState import org.opalj.tac.fpcf.analyses.fieldassignability.part.ClonePatternAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.part.LazyInitializationAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.part.LazyInitializationAnalysisState @@ -24,10 +25,11 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som extends AbstractFieldAssignabilityAnalysis with LazyInitializationAnalysis with ClonePatternAnalysis - with ExtensiveReadWritePathAnalysis { + with CalleesBasedReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState with LazyInitializationAnalysisState + with CalleesBasedReadWritePathAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala index f9a14527dc..d30e5babb6 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala @@ -6,11 +6,17 @@ package analyses package fieldassignability package part +import org.opalj.br.DeclaredMethod import org.opalj.br.PC +import org.opalj.br.fpcf.analyses.ContextProvider import org.opalj.br.fpcf.properties.Context +import org.opalj.br.fpcf.properties.cg.Callees import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.NonAssignable +import org.opalj.fpcf.EOptionP +import org.opalj.fpcf.SomeEPS +import org.opalj.fpcf.UBP import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** @@ -28,21 +34,26 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] registerPart(PartInfo( onInitializerRead = onInitializerRead(_, _, _, _)(using _), - onInitializerWrite = onInitializerWrite(_, _, _, _)(using _) + onInitializerWrite = onInitializerWrite(_, _, _, _)(using _), + continuation = onContinue(_)(using _) )) /** * Allows users of this trait to specify whether the write context is provably unreachable from the read context. - * Implementations must be sound, and abort with 'false' if the result cannot be determined at the current time. + * Implementations must be soundy, i.e. abort with 'false' when the framework does not provide enough information to + * eventually compute a precise answer. * * @note Assumes that the two contexts point to different methods, i.e. that intraprocedural path existence is * handled separately. */ protected def isContextUnreachableFrom( + readPC: Int, readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean + protected def onContinue(eps: SomeEPS)(implicit state: AnalysisState): Option[FieldAssignability] = None + private def onInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], @@ -57,7 +68,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] if (state.initializerWrites.exists { case (writeContext, _) => (writeContext.method ne context.method) && - !isContextUnreachableFrom(context, writeContext) + !isContextUnreachableFrom(readPC, context, writeContext) } ) return Some(Assignable); @@ -97,9 +108,9 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] } if (state.initializerReads.exists { - case (readContext, _) => + case (readContext, readAccesses) => (readContext.method ne context.method) && - !isContextUnreachableFrom(readContext, context) + readAccesses.exists(access => !isContextUnreachableFrom(access._1, readContext, context)) } ) return Some(Assignable); @@ -128,7 +139,8 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] */ trait SimpleReadWritePathAnalysis private[fieldassignability] extends ReadWritePathAnalysisPart { - override protected final def isContextUnreachableFrom( + override protected def isContextUnreachableFrom( + readPC: Int, readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean = false @@ -141,23 +153,114 @@ trait SimpleReadWritePathAnalysis private[fieldassignability] extends ReadWriteP * is in the same class as the field declaration. */ trait ExtensiveReadWritePathAnalysis private[fieldassignability] - extends ReadWritePathAnalysisPart { + extends SimpleReadWritePathAnalysis { + + override protected def isContextUnreachableFrom( + readPC: Int, + readContext: Context, + writeContext: Context + )(implicit state: AnalysisState): Boolean = { + if (super.isContextUnreachableFrom(readPC, readContext, writeContext)) + return true; + + val writeMethod = writeContext.method.definedMethod + // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before + // the reading method, which is in turn only guaranteed when the writing static initializer is in the same + // type as the field declaration, i.e. it is run when the field is used for the first time. + + // Note that this also implies that there may be no supertype static initializer that can read the field, + // preventing read-write paths in the inheritance chain. + writeMethod.isStaticInitializer && (writeContext.method.declaringClassType eq state.field.classFile.thisType) + } +} + +trait CalleesBasedReadWritePathAnalysisState extends AbstractFieldAssignabilityAnalysisState { + private[part] var calleeDependees: Map[Context, EOptionP[DeclaredMethod, Callees]] = Map.empty +} + +/** + * Based on failure of the [[ExtensiveReadWritePathAnalysis]], marks two constructor method contexts as unreachable + * when (based on the available call graph) the reading constructor does either not invoke any other constructors or + * any such invocation comes after the read. + * + * This result is considered sound as once a call string leaves a constructor, it cannot reenter a constructor on the + * same instance. Thus, when the reading constructor does not call any other constructor, it is impossible for the + * instance under construction to reach the writing constructor. + */ +trait CalleesBasedReadWritePathAnalysis private[fieldassignability] + extends ExtensiveReadWritePathAnalysis { + + override type AnalysisState <: CalleesBasedReadWritePathAnalysisState + + implicit val contextProvider: ContextProvider override protected final def isContextUnreachableFrom( + readPC: Int, readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean = { + if (super.isContextUnreachableFrom(readPC, readContext, writeContext)) + return true; + + val readMethod = readContext.method.definedMethod val writeMethod = writeContext.method.definedMethod - if (state.field.isStatic && writeMethod.isStaticInitializer) { - // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before - // the reading method, which is in turn only guaranteed when the writing static initializer is in the same - // type as the field declaration, i.e. it is run when the field is used for the first time. - - // Note that this also implies that there may be no supertype static initializer that can read the field, - // preventing read-write paths in the inheritance chain. - writeContext.method.declaringClassType eq state.field.classFile.thisType + if (!readMethod.isConstructor || !writeMethod.isConstructor) + return false; + + val readCalleesProperty = state.calleeDependees.get(readContext) match { + case Some(callees) => callees + case None => + val callees = ps(readContext.method, Callees.key) + state.calleeDependees = state.calleeDependees.updated(readContext, callees) + callees + } + + if (readCalleesProperty.hasUBP) { + if (readCalleesProperty.ub.hasIncompleteCallSites(readContext)) { + false + } else { + val tac = state.tacDependees(readContext.method.asDefinedMethod).ub.tac.get + + readCalleesProperty.ub.callSites(readContext).forall { + case (callSitePC, potentialCallees) => + // Either the reading constructor is not calling any other constructors ... + potentialCallees.forall(!_.method.definedMethod.isConstructor) || + // or the call is irrelevant since there is no way it is executed after the read + !pathExists(readPC, callSitePC, tac) + } + } } else { - false + true + } + } + + override protected def onContinue(eps: SomeEPS)(implicit state: AnalysisState): Option[FieldAssignability] = { + eps match { + case UBP(callees: Callees) => + val contexts = state.calleeDependees.iterator.filter(_._2.e eq eps.e).map(_._1).toSeq + val dangerousCallSiteFound = contexts.exists { context => + state.calleeDependees = + state.calleeDependees.updated(context, eps.asInstanceOf[EOptionP[DeclaredMethod, Callees]]) + + callees.hasIncompleteCallSites(context) || { + val tac = state.tacDependees(context.method.asDefinedMethod).ub.tac.get + callees.callSites(context).exists { + case (callSitePC, potentialCallees) => + // Either the reading constructor is not calling any other constructors ... + potentialCallees.exists(_.method.definedMethod.isConstructor) || + // or the call is irrelevant since there is no read after which it is executed + state.initializerReads(context).exists(read => pathExists(read._1, callSitePC, tac)) + } + } + } + + if (dangerousCallSiteFound) + Some(Assignable) + else + Some(NonAssignable) + + case _ => + super.onContinue(eps) } } } From a1dc7d3d3a204b82f9670583e810be890a41e5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sat, 29 Nov 2025 14:56:59 +0100 Subject: [PATCH 38/41] Recognize getClass calls as safe --- .../DangerousCallsInConstructor.java | 29 +++++++++++++++++++ .../part/FieldAssignabilityAnalysisPart.scala | 3 ++ 2 files changed, 32 insertions(+) create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DangerousCallsInConstructor.java diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DangerousCallsInConstructor.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DangerousCallsInConstructor.java new file mode 100644 index 0000000000..f4ac788ed4 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/DangerousCallsInConstructor.java @@ -0,0 +1,29 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.assignability; + +import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; +import org.opalj.fpcf.properties.immutability.field_assignability.EffectivelyNonAssignableField; + +class ArbitraryCallInConstructor { + + @AssignableField("The presence of arbitrary calls prevents guaranteeing that no read-write paths exist.") + private boolean value; + + public ArbitraryCallInConstructor(boolean v) { + this.arbitraryCallee(); + this.value = v; + } + + public void arbitraryCallee() {} +} + +class GetClassInConstructor { + + @EffectivelyNonAssignableField("The field is only assigned once in its own constructor.") + private boolean value; + + public GetClassInConstructor(boolean v) { + System.out.println(this.getClass()); + this.value = v; + } +} diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala index 1ece2a99b8..04f36afd3e 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala @@ -79,6 +79,9 @@ trait FieldAssignabilityAnalysisPart private[fieldassignability] // ... we identify easy instances of field reads (interaction with potentially disruptive reads // is checked separately; note that this inference relies on a flat TAC) OR ... stmt.isAssignment && stmt.asAssignment.expr.isGetField || + // ... we identify easy instances of calls to the native final method "getClass", which we assume to be safe + // even though its implementation is fixed by the JVM spec OR ... + stmt.isAssignment && stmt.asAssignment.expr.isVirtualFunctionCall && stmt.asAssignment.expr.asVirtualFunctionCall.name == "getClass" || // ... we easily identify the use site to initialize an object (fine as initializer reads outside the // current method are forbidden) OR ... stmt.isMethodCall && stmt.asMethodCall.name == "" || From f96236d733f27d02b17119b533b6bb2c65ff811d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sat, 29 Nov 2025 17:24:23 +0100 Subject: [PATCH 39/41] Add advanced counter examples --- .../PrematurelyReadFinalField.java | 35 +++++++++++++++++++ .../ThisEscapesDuringConstruction.java | 23 ++++++++++++ .../ValueReadBeforeAssignment.java | 25 +++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/PrematurelyReadFinalField.java create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ThisEscapesDuringConstruction.java create mode 100644 DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ValueReadBeforeAssignment.java diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/PrematurelyReadFinalField.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/PrematurelyReadFinalField.java new file mode 100644 index 0000000000..b94859d7e5 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/PrematurelyReadFinalField.java @@ -0,0 +1,35 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.assignability.advanced_counter_examples; + +import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; + +/** + * The default value of the field x is assigned to another field n during construction and as + * a result seen with two different values. + */ +public class PrematurelyReadFinalField { + + @AssignableField("Field n is assigned with different values.") + static int n = 5; + + public static void main(String[] args) { + C c = new C(); + } +} + +class B { + B() { + PrematurelyReadFinalField.n = ((C) this).x; + } +} + +class C extends B { + + @AssignableField("Is seen with two different values during construction.") + public final int x; + + C() { + super(); + x = 3; + } +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ThisEscapesDuringConstruction.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ThisEscapesDuringConstruction.java new file mode 100644 index 0000000000..27e4cffef7 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ThisEscapesDuringConstruction.java @@ -0,0 +1,23 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.assignability.advanced_counter_examples; + +import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; + +/** + * This test case simulates the fact that the `this` object escapes in the constructor before (final) fields + * are assigned. + */ +public class ThisEscapesDuringConstruction { + + @AssignableField("The this object escapes in the constructor before the field is assigned.") + final int n; + + public ThisEscapesDuringConstruction() { + C2.m(this); + n = 7; + } +} + +class C2 { + public static void m(ThisEscapesDuringConstruction c) {} +} diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ValueReadBeforeAssignment.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ValueReadBeforeAssignment.java new file mode 100644 index 0000000000..57d7c485f9 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/assignability/advanced_counter_examples/ValueReadBeforeAssignment.java @@ -0,0 +1,25 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj.fpcf.fixtures.immutability.openworld.assignability.advanced_counter_examples; + +import org.opalj.fpcf.properties.immutability.field_assignability.AssignableField; + +/** + * The value of the field x is read with its default value (0) + * in the constructor before assignment and assigned to a public field. + * Thus, the value can be accessed from everywhere. + */ +public class ValueReadBeforeAssignment { + @AssignableField("Field value is read before assignment.") + private int x; + @AssignableField("Field y is public and not final.") + public int y; + + public ValueReadBeforeAssignment() { + y = x; + x = 42; + } + + public ValueReadBeforeAssignment foo() { + return new ValueReadBeforeAssignment(); + } +} From 7c60006b53b568e92cf75af664f85a5c23897d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sat, 29 Nov 2025 17:28:06 +0100 Subject: [PATCH 40/41] Remove unsound callee based read write path analysis --- .../stringelements/SimpleStringModel.java | 5 +- .../L1FieldAssignabilityAnalysis.scala | 6 +- .../L2FieldAssignabilityAnalysis.scala | 6 +- .../part/ReadWritePathAnalysisPart.scala | 96 ------------------- 4 files changed, 5 insertions(+), 108 deletions(-) diff --git a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java index e9b68c6850..374d6c24d8 100644 --- a/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/stringelements/SimpleStringModel.java @@ -5,10 +5,8 @@ import org.opalj.fpcf.properties.immutability.field_assignability.UnsafelyLazilyInitializedField; import org.opalj.fpcf.properties.immutability.fields.MutableField; import org.opalj.fpcf.properties.immutability.fields.TransitivelyImmutableField; -import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; import org.opalj.fpcf.properties.immutability.field_assignability.LazilyInitializedField; import org.opalj.tac.fpcf.analyses.FieldImmutabilityAnalysis; -import org.opalj.tac.fpcf.analyses.fieldassignability.L1FieldAssignabilityAnalysis; import org.opalj.tac.fpcf.analyses.fieldassignability.L2FieldAssignabilityAnalysis; /** @@ -20,8 +18,7 @@ public final class SimpleStringModel { @TransitivelyImmutableField(value = "The array values are not mutated after the assignment ", analyses = {}) @MutableField(value = "The analysis can not recognize transitive immutable arrays, and the field is assignable", analyses = { FieldImmutabilityAnalysis.class }) - @NonAssignableField(value = "The field is final and it is written in two different initializers that do not call each other", - analyses = { L1FieldAssignabilityAnalysis.class, L2FieldAssignabilityAnalysis.class }) + @AssignableField(value = "The field is read/written in two different initializers and the analysis does not consider whether they call each other") private final char value[]; public char[] getValue() { diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala index 5828cf81f0..eb3772fac3 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L1FieldAssignabilityAnalysis.scala @@ -9,8 +9,7 @@ import org.opalj.br.Field import org.opalj.br.analyses.SomeProject import org.opalj.br.fpcf.properties.cg.Callees import org.opalj.fpcf.PropertyBounds -import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysisState +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis /** * Determines the assignability of a field based on a more complex analysis of read-write paths than @@ -23,10 +22,9 @@ import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWrite */ class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - with CalleesBasedReadWritePathAnalysis { + with ExtensiveReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState - with CalleesBasedReadWritePathAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala index 2f1686b3e6..74574abeb8 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/L2FieldAssignabilityAnalysis.scala @@ -7,9 +7,8 @@ package fieldassignability import org.opalj.br.Field import org.opalj.br.analyses.SomeProject -import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysis -import org.opalj.tac.fpcf.analyses.fieldassignability.part.CalleesBasedReadWritePathAnalysisState import org.opalj.tac.fpcf.analyses.fieldassignability.part.ClonePatternAnalysis +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.part.LazyInitializationAnalysis import org.opalj.tac.fpcf.analyses.fieldassignability.part.LazyInitializationAnalysisState @@ -25,11 +24,10 @@ class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: Som extends AbstractFieldAssignabilityAnalysis with LazyInitializationAnalysis with ClonePatternAnalysis - with CalleesBasedReadWritePathAnalysis { + with ExtensiveReadWritePathAnalysis { case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState with LazyInitializationAnalysisState - with CalleesBasedReadWritePathAnalysisState type AnalysisState = State override def createState(field: Field): AnalysisState = State(field) } diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala index d30e5babb6..e781fb3e58 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala @@ -6,17 +6,12 @@ package analyses package fieldassignability package part -import org.opalj.br.DeclaredMethod import org.opalj.br.PC -import org.opalj.br.fpcf.analyses.ContextProvider import org.opalj.br.fpcf.properties.Context -import org.opalj.br.fpcf.properties.cg.Callees import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.NonAssignable -import org.opalj.fpcf.EOptionP import org.opalj.fpcf.SomeEPS -import org.opalj.fpcf.UBP import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** @@ -173,94 +168,3 @@ trait ExtensiveReadWritePathAnalysis private[fieldassignability] writeMethod.isStaticInitializer && (writeContext.method.declaringClassType eq state.field.classFile.thisType) } } - -trait CalleesBasedReadWritePathAnalysisState extends AbstractFieldAssignabilityAnalysisState { - private[part] var calleeDependees: Map[Context, EOptionP[DeclaredMethod, Callees]] = Map.empty -} - -/** - * Based on failure of the [[ExtensiveReadWritePathAnalysis]], marks two constructor method contexts as unreachable - * when (based on the available call graph) the reading constructor does either not invoke any other constructors or - * any such invocation comes after the read. - * - * This result is considered sound as once a call string leaves a constructor, it cannot reenter a constructor on the - * same instance. Thus, when the reading constructor does not call any other constructor, it is impossible for the - * instance under construction to reach the writing constructor. - */ -trait CalleesBasedReadWritePathAnalysis private[fieldassignability] - extends ExtensiveReadWritePathAnalysis { - - override type AnalysisState <: CalleesBasedReadWritePathAnalysisState - - implicit val contextProvider: ContextProvider - - override protected final def isContextUnreachableFrom( - readPC: Int, - readContext: Context, - writeContext: Context - )(implicit state: AnalysisState): Boolean = { - if (super.isContextUnreachableFrom(readPC, readContext, writeContext)) - return true; - - val readMethod = readContext.method.definedMethod - val writeMethod = writeContext.method.definedMethod - if (!readMethod.isConstructor || !writeMethod.isConstructor) - return false; - - val readCalleesProperty = state.calleeDependees.get(readContext) match { - case Some(callees) => callees - case None => - val callees = ps(readContext.method, Callees.key) - state.calleeDependees = state.calleeDependees.updated(readContext, callees) - callees - } - - if (readCalleesProperty.hasUBP) { - if (readCalleesProperty.ub.hasIncompleteCallSites(readContext)) { - false - } else { - val tac = state.tacDependees(readContext.method.asDefinedMethod).ub.tac.get - - readCalleesProperty.ub.callSites(readContext).forall { - case (callSitePC, potentialCallees) => - // Either the reading constructor is not calling any other constructors ... - potentialCallees.forall(!_.method.definedMethod.isConstructor) || - // or the call is irrelevant since there is no way it is executed after the read - !pathExists(readPC, callSitePC, tac) - } - } - } else { - true - } - } - - override protected def onContinue(eps: SomeEPS)(implicit state: AnalysisState): Option[FieldAssignability] = { - eps match { - case UBP(callees: Callees) => - val contexts = state.calleeDependees.iterator.filter(_._2.e eq eps.e).map(_._1).toSeq - val dangerousCallSiteFound = contexts.exists { context => - state.calleeDependees = - state.calleeDependees.updated(context, eps.asInstanceOf[EOptionP[DeclaredMethod, Callees]]) - - callees.hasIncompleteCallSites(context) || { - val tac = state.tacDependees(context.method.asDefinedMethod).ub.tac.get - callees.callSites(context).exists { - case (callSitePC, potentialCallees) => - // Either the reading constructor is not calling any other constructors ... - potentialCallees.exists(_.method.definedMethod.isConstructor) || - // or the call is irrelevant since there is no read after which it is executed - state.initializerReads(context).exists(read => pathExists(read._1, callSitePC, tac)) - } - } - } - - if (dangerousCallSiteFound) - Some(Assignable) - else - Some(NonAssignable) - - case _ => - super.onContinue(eps) - } - } -} From 670546667ecb59cc78757192ddded4eff0666172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Sat, 29 Nov 2025 17:32:04 +0100 Subject: [PATCH 41/41] Remove unused simple read write analysis --- .../part/ReadWritePathAnalysisPart.scala | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala index e781fb3e58..c69727f0dc 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala @@ -11,7 +11,6 @@ import org.opalj.br.fpcf.properties.Context import org.opalj.br.fpcf.properties.immutability.Assignable import org.opalj.br.fpcf.properties.immutability.FieldAssignability import org.opalj.br.fpcf.properties.immutability.NonAssignable -import org.opalj.fpcf.SomeEPS import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites /** @@ -29,8 +28,7 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] registerPart(PartInfo( onInitializerRead = onInitializerRead(_, _, _, _)(using _), - onInitializerWrite = onInitializerWrite(_, _, _, _)(using _), - continuation = onContinue(_)(using _) + onInitializerWrite = onInitializerWrite(_, _, _, _)(using _) )) /** @@ -47,8 +45,6 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] writeContext: Context )(implicit state: AnalysisState): Boolean - protected def onContinue(eps: SomeEPS)(implicit state: AnalysisState): Option[FieldAssignability] = None - private def onInitializerRead( context: Context, tac: TACode[TACMethodParameter, V], @@ -127,37 +123,20 @@ sealed trait ReadWritePathAnalysisPart private[fieldassignability] } } -/** - * @inheritdoc - * - * The simple path analysis considers every interprocedural path to be harmful, causing the field to be assignable. - */ -trait SimpleReadWritePathAnalysis private[fieldassignability] extends ReadWritePathAnalysisPart { - - override protected def isContextUnreachableFrom( - readPC: Int, - readContext: Context, - writeContext: Context - )(implicit state: AnalysisState): Boolean = false -} - /** * @inheritdoc * * The extensive path analysis considers interprocedural static field writes as safe when the writing static initializer - * is in the same class as the field declaration. + * is in the same class as the field declaration, and otherwise soundly aborts. */ trait ExtensiveReadWritePathAnalysis private[fieldassignability] - extends SimpleReadWritePathAnalysis { + extends ReadWritePathAnalysisPart { override protected def isContextUnreachableFrom( readPC: Int, readContext: Context, writeContext: Context )(implicit state: AnalysisState): Boolean = { - if (super.isContextUnreachableFrom(readPC, readContext, writeContext)) - return true; - val writeMethod = writeContext.method.definedMethod // We can only guarantee that no paths exist when the writing static initializer is guaranteed to run before // the reading method, which is in turn only guaranteed when the writing static initializer is in the same