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/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/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; + } +} 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/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(); + } +} 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/general/StaticFieldWithDefaultValue.java b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/StaticFieldWithDefaultValue.java new file mode 100644 index 0000000000..72d04b5756 --- /dev/null +++ b/DEVELOPING_OPAL/validate/src/test/java/org/opalj/fpcf/fixtures/immutability/openworld/general/StaticFieldWithDefaultValue.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.TransitivelyImmutableClass; +import org.opalj.fpcf.properties.immutability.field_assignability.NonAssignableField; +import org.opalj.fpcf.properties.immutability.fields.NonTransitivelyImmutableField; +import org.opalj.fpcf.properties.immutability.types.MutableType; + +@MutableType("The type is extensible") +@TransitivelyImmutableClass("Class has no instance fields") +public class StaticFieldWithDefaultValue { + + @NonTransitivelyImmutableField("Field is not assignable") + @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 StaticFieldWithDefaultValue() { + System.out.println(StaticFieldWithDefaultValue.DEFAULT); + } +} 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/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; + } +} 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 1446c3b19d..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 @@ -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 recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -36,7 +36,7 @@ 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 recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -68,7 +68,7 @@ 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 recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + analyses = { L2FieldAssignabilityAnalysis.class }) private int x; public int init() { @@ -85,7 +85,7 @@ 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 recognize determinism", - analyses = {L2FieldAssignabilityAnalysis.class}) + 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() { @@ -296,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) { @@ -312,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; + } +} 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..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,46 +1,43 @@ /* 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; 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 = { 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 = { 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/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 bc31baf84e..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 @@ -1,10 +1,10 @@ /* 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.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.L2FieldAssignabilityAnalysis; @@ -16,33 +16,33 @@ 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") - private final char[] value; + @MutableField(value = "The analysis can not recognize transitive immutable arrays, and the field is assignable", + analyses = { FieldImmutabilityAnalysis.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(){ + 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(); } public int hashCode() { int h = 0; if (hash == 0) { - char[] val = value; + char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } 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 1006d70166..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 @@ -22,6 +25,7 @@ import org.opalj.tac.fpcf.analyses.fieldassignability.EagerL2FieldAssignabilityA * Tests the field assignability analysis * * @author Tobias Roth + * @author Maximilian Rüsch */ class FieldAssignabilityTests extends PropertiesTest { @@ -34,14 +38,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]]) + import org.opalj.ai.domain + Set[Class[? <: AnyRef]](classOf[domain.l1.DefaultDomainWithCFGAndDefUse[URL]]) } p.get(RTACallGraphKey) - } describe("no analysis is scheduled") { @@ -50,8 +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, @@ -62,11 +64,15 @@ class FieldAssignabilityTests extends PropertiesTest { ) ) as.propertyStore.shutdown() - validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) - } + l0 = Some(as.project, as.propertyStore) - describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executed") { + describe("and produces the correct properties") { + validateProperties(as, fieldsWithAnnotations(as.project), Set("FieldAssignability")) + } + } + var l1: Option[(SomeProject, PropertyStore)] = None + describe("the org.opalj.fpcf.analyses.L1FieldAssignability is executable") { val as = executeAnalyses( Set( EagerL1FieldAssignabilityAnalysis, @@ -77,11 +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")) + } - describe("the org.opalj.fpcf.analyses.L2FieldAssignability is executed") { + 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 executable") { val as = executeAnalyses( Set( EagerL2FieldAssignabilityAnalysis, @@ -92,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")}") } } 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..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 @@ -34,12 +34,12 @@ 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 { - var notEscapes: Boolean = false - final val PropertyKeyName = "opalj.FieldAssignability" final val key: PropertyKey[FieldAssignability] = { @@ -73,9 +73,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/main/scala/org/opalj/tac/Stmt.scala b/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala index 9d130f92e2..2d265910c5 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/Stmt.scala @@ -92,6 +92,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 @@ -657,6 +658,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 828217f15b..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 @@ -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,51 +34,85 @@ 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.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.analyses.fieldassignability.part.PartAnalysisAbstractions import org.opalj.tac.fpcf.properties.TACAI -import org.opalj.value.ValueInformation -trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis { +trait AbstractFieldAssignabilityAnalysisState { - trait AbstractFieldAssignabilityAnalysisState { + val field: Field + var fieldAssignability: FieldAssignability = NonAssignable - 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 tacDependees: Map[DefinedMethod, EOptionP[Method, TACAI]] = Map.empty - var callerDependees: Map[DefinedMethod, EOptionP[DefinedMethod, Callers]] = Map.empty.withDefault { dm => - propertyStore(dm, Callers.key) - } + 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) - def hasDependees: Boolean = { - escapeDependees.nonEmpty || fieldWriteAccessDependee.exists(_.isRefinable) || - tacDependees.valuesIterator.exists(_.isRefinable) || callerDependees.valuesIterator.exists(_.isRefinable) - } + 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) - def dependees: Set[SomeEOptionP] = { - escapeDependees ++ fieldWriteAccessDependee.filter(_.isRefinable) ++ - callerDependees.valuesIterator.filter(_.isRefinable) ++ tacDependees.valuesIterator.filter(_.isRefinable) - } + 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) + } +} + +/** + * Base trait for all field assignability analyses. Analyses are comprised of a sequence of + * [[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 [[part.FieldAssignabilityAnalysisPart]] + * + * @author Maximilian Rüsch + * @author Dominik Helm + */ +trait AbstractFieldAssignabilityAnalysis extends FPCFAnalysis with PartAnalysisAbstractions { + + private var parts: Seq[PartInfo] = Seq.empty + + override def registerPart(partInfo: PartInfo): Unit = { + parts = parts :+ partInfo } - type V = DUVar[ValueInformation] + private def determineAssignabilityFromParts( + hookFunc: PartInfo => Option[FieldAssignability] + ): Option[FieldAssignability] = { + var assignability: Option[FieldAssignability] = None + for { + partInfo <- parts + if !assignability.contains(Assignable) + partAssignability = hookFunc(partInfo) + if partAssignability.isDefined + } { + if (assignability.isDefined) + assignability = Some(assignability.get.meet(partAssignability.get)) + else + assignability = Some(partAssignability.get) + } + + assignability + } final val typeExtensibility = project.get(TypeExtensibilityKey) final val closedPackages = project.get(ClosedPackagesKey) @@ -104,157 +126,160 @@ 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) } } - type AnalysisState <: AbstractFieldAssignabilityAnalysisState + override type AnalysisState <: AbstractFieldAssignabilityAnalysisState 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); + handleWriteAccessInformation(propertyStore(declaredFields(field), FieldWriteAccessInformation.key))(using state) + } - createResult() + private def analyzeInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = { + val result = determineAssignabilityFromParts(_.onInitializerRead(context, tac, readPC, receiver, state)) + result.getOrElse(NonAssignable) + } + + private def analyzeNonInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = { + val result = determineAssignabilityFromParts(_.onNonInitializerRead(context, tac, readPC, receiver, state)) + result.getOrElse(NonAssignable) + } + + private def analyzeInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = { + val result = determineAssignabilityFromParts(_.onInitializerWrite(context, tac, writePC, receiver, state)) + result.getOrElse(Assignable) + } + + private def analyzeNonInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): FieldAssignability = { + val result = determineAssignabilityFromParts(_.onNonInitializerWrite(context, tac, writePC, receiver, state)) + result.getOrElse(Assignable) } - /** - * 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 - - case InterimUBP(NoEscape | EscapeInCallee | EscapeViaReturn) => - state.escapeDependees += ep - false - - case InterimUBP(AtMost(_)) => - true - - case _: SomeInterimEP => - true // Escape state is worse than via return - - case _ => - 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) } - /** - * 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) + def continuation(eps: SomeEPS)(implicit state: AnalysisState): ProperPropertyComputationResult = { + eps.pk match { + case FieldWriteAccessInformation.key => + handleWriteAccessInformation(eps.asInstanceOf[EOptionP[DeclaredField, FieldWriteAccessInformation]]) + + case FieldReadAccessInformation.key => + handleReadAccessInformation(eps.asInstanceOf[EOptionP[DeclaredField, FieldReadAccessInformation]]) + + 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 + + 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) + } + } + } } - !hasEscaped } - } + + refreshAssignability(state.initializerReads, analyzeInitializerRead) + refreshAssignability(state.nonInitializerReads, analyzeNonInitializerRead) + refreshAssignability(state.initializerWrites, analyzeInitializerWrite) + refreshAssignability(state.nonInitializerWrites, analyzeNonInitializerWrite) + + 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 @@ -263,112 +288,86 @@ 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) - } - - if (isNonFinal) - Result(state.field, Assignable) - else - createResult() - } + 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))) + } + } - def createResult()(implicit state: AnalysisState): ProperPropertyComputationResult = { - if (state.hasDependees && (state.fieldAssignability ne Assignable)) { - InterimResult( - state.field, - Assignable, - state.fieldAssignability, - state.dependees, - c - ) + 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 { - Result(state.field, state.fieldAssignability) + state.fieldReadAccessDependee = Some(newEP) } - } - /** - * Returns the initialization value of a given type. - */ - 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) + createResult() } } -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( @@ -382,3 +381,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..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 @@ -5,213 +5,31 @@ 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.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.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.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis /** - * 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. + * 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 */ -class L0FieldAssignabilityAnalysis private[analyses] (val project: SomeProject) extends FPCFAnalysis { +class L0FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) + extends AbstractFieldAssignabilityAnalysis + with ExtensiveReadWritePathAnalysis { - 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() - } + case class State(field: Field) extends AbstractFieldAssignabilityAnalysisState + type AnalysisState = State + override def createState(field: Field): AnalysisState = State(field) } -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 index 1d0ed2eb20..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 @@ -5,106 +5,36 @@ 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.br.fpcf.properties.cg.Callees import org.opalj.fpcf.PropertyBounds -import org.opalj.fpcf.PropertyStore -import org.opalj.tac.fpcf.analyses.cg.uVarForDefSites +import org.opalj.tac.fpcf.analyses.fieldassignability.part.ExtensiveReadWritePathAnalysis /** - * 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. + * 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. - * @author Tobias Roth + * @note May soundly overapproximate the assignability if the TAC is deeply nested. + * + * @author Maximilian Rüsch * @author Dominik Helm - * @author Florian Kübler - * @author Michael Eichberg */ -class L1FieldAssignabilityAnalysis private[analyses] (val project: SomeProject) - extends AbstractFieldAssignabilityAnalysis { +class L1FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) + extends AbstractFieldAssignabilityAnalysis + with ExtensiveReadWritePathAnalysis { 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) +object EagerL1FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def uses: Set[PropertyBounds] = super.uses + PropertyBounds.ub(Callees) - 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 - } + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L1FieldAssignabilityAnalysis(p) } -/** - * 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 - } +object LazyL1FieldAssignabilityAnalysis extends AbstractLazyFieldAssignabilityAnalysisScheduler { + 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 7b1e59e603..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 @@ -5,1036 +5,37 @@ 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.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.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 +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. + * 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[analyses] (val project: SomeProject) +class L2FieldAssignabilityAnalysis private[fieldassignability] (val project: SomeProject) extends AbstractFieldAssignabilityAnalysis - 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) - } + 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) - - /** - * 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 afterward - */ - 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) - 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)) - 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(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 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 - 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) - - /** - * 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 - } - } 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) { - (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 { - - 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 - - // 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. - */ - 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 - - 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. - */ - 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) - } - } - } - - // 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. - */ - 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)) - } - } - } - } - } - } - } - } - -} - -trait L2FieldAssignabilityAnalysisScheduler extends AbstractFieldAssignabilityAnalysisScheduler { - override def uses: Set[PropertyBounds] = super.uses ++ PropertyBounds.ubs(FieldReadAccessInformation) } -/** - * 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 - } +object EagerL2FieldAssignabilityAnalysis extends AbstractEagerFieldAssignabilityAnalysisScheduler { + override def newAnalysis(p: SomeProject): AbstractFieldAssignabilityAnalysis = new L2FieldAssignabilityAnalysis(p) } -/** - * 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/part/ClonePatternAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala new file mode 100644 index 0000000000..f294c899e1 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ClonePatternAnalysis.scala @@ -0,0 +1,96 @@ +/* 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.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 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. + * + * @author Maximilian Rüsch + */ +trait ClonePatternAnalysis private[fieldassignability] + extends FieldAssignabilityAnalysisPart { + + registerPart(PartInfo( + onNonInitializerRead = onNonInitializerRead(_, _, _, _)(using _), + onNonInitializerWrite = onNonInitializerWrite(_, _, _, _)(using _) + )) + + private def onNonInitializerRead( + context: Context, + 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 + } + + private def onNonInitializerWrite( + 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/part/FieldAssignabilityAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala new file mode 100644 index 0000000000..04f36afd3e --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/FieldAssignabilityAnalysisPart.scala @@ -0,0 +1,91 @@ +/* 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.fpcf.FPCFAnalysis + +/** + * 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 with PartAnalysisAbstractions { + + 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 + * 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 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 == "" || + // ... 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/part/LazyInitializationAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/LazyInitializationAnalysis.scala new file mode 100644 index 0000000000..3ea72260d4 --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/LazyInitializationAnalysis.scala @@ -0,0 +1,800 @@ +/* BSD 2-Clause License - see OPAL/LICENSE for details. */ +package org.opalj +package tac +package fpcf +package analyses +package fieldassignability +package part + +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.cfg.BasicBlock +import org.opalj.br.cfg.CFGNode +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 + +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. + * + * @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 FieldAssignabilityAnalysisPart { + + 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" + ) + + /** + * 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) + } + + /** + * 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 + } + + private def onInitializerRead( + context: Context, + tac: TACode[TACMethodParameter, V], + readPC: PC, + receiver: Option[V] + )(implicit state: AnalysisState): Option[FieldAssignability] = + state.potentialLazyInit.map(_ => Assignable) + + private def onNonInitializerRead( + 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) + 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 Some(Assignable); + if (context.id != lazyInitContext.id) + return None; + + if (doFieldReadsEscape(Set(readPC), guardIndex, writeIndex, tac)) + return Some(Assignable); + + None + } + + private def onNonInitializerWrite( + 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; + } + + // Multiple lazy initialization patterns cannot be supported in a collaborative setting + if (state.nonInitializerWrites.iterator.distinctBy(_._1.method).size > 1) + return Some(Assignable); + + // We do not support multiple-write lazy initializations yet + if (state.nonInitializerWrites(context).size > 1) + 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 Some(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 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 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 Some(Assignable); + + val elseBB = cfg.bb(elseCaseIndex) + + // prevents wrong control flow + if (isTransitivePredecessor(elseBB, writeBB)) + 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 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 + 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 Some(Assignable); + } + + if (doFieldReadsEscape(state.nonInitializerReads(context).map(_._1), guardIndex, writeIndex, tac)) + return Some(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) + Some(LazilyInitialized) + else { + val (indexMonitorEnter, indexMonitorExit) = findMonitors(writeIndex, tac) + val monitorResultsDefined = indexMonitorEnter.isDefined && indexMonitorExit.isDefined + if (monitorResultsDefined && dominates(indexMonitorEnter.get, readIndex, tac)) + Some(LazilyInitialized) + else + Some(UnsafelyLazilyInitialized) + } + } else + Some(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)) + } + } + } + } + } + } + } + } +} 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..ac47439f60 --- /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/part/ReadWritePathAnalysisPart.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala new file mode 100644 index 0000000000..c69727f0dc --- /dev/null +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/fieldassignability/part/ReadWritePathAnalysisPart.scala @@ -0,0 +1,149 @@ +/* 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.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 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 (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 + * the two instances are disjoint. + * + * @author Maximilian Rüsch + */ +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 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 + + private def onInitializerRead( + 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); + + // 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) && + !isContextUnreachableFrom(readPC, 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) + } + + if (pathFromReadToSomeWriteExists) + Some(Assignable) + else + Some(NonAssignable) + } + + private def onInitializerWrite( + context: Context, + tac: TACode[TACMethodParameter, V], + writePC: PC, + 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); + + // 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); + } + + if (state.initializerReads.exists { + case (readContext, readAccesses) => + (readContext.method ne context.method) && + readAccesses.exists(access => !isContextUnreachableFrom(access._1, readContext, context)) + } + ) + 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) + } +} + +/** + * @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, and otherwise soundly aborts. + */ +trait ExtensiveReadWritePathAnalysis private[fieldassignability] + extends ReadWritePathAnalysisPart { + + override protected def isContextUnreachableFrom( + readPC: Int, + readContext: Context, + writeContext: Context + )(implicit state: AnalysisState): Boolean = { + 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) + } +}