import java.util.Random; import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.*; import org.checkerframework.dataflow.qual.SideEffectFree; public class TestTreeKinds { class MyClass { Object field = new Object(); @LockingFree Object method(@GuardSatisfied MyClass this) { return new Object(); } void method2(@GuardSatisfied MyClass this) {} } @GuardedBy("lock") MyClass m; { // In constructor/initializer, it's OK not to hold the lock on 'this', but other locks must be respected. //:: error: (lock.not.held) m.field = new Object(); } final ReentrantLock lock = new ReentrantLock(); final ReentrantLock lock2 = new ReentrantLock(); @GuardedBy("lock") MyClass foo = new MyClass(); MyClass unguardedFoo = new MyClass(); @EnsuresLockHeld("lock") void lockTheLock() { lock.lock(); } @EnsuresLockHeld("lock2") void lockTheLock2() { lock2.lock(); } @EnsuresLockHeldIf(expression = "lock", result = true) boolean tryToLockTheLock() { return lock.tryLock(); } // This @MayReleaseLocks annotation causes dataflow analysis to assume 'lock' is released after unlockTheLock() is called. @MayReleaseLocks void unlockTheLock() {} @SideEffectFree void sideEffectFreeMethod() {} @LockingFree void lockingFreeMethod() {} @MayReleaseLocks void nonSideEffectFreeMethod() {} @Holding("lock") void requiresLockHeldMethod() {} MyClass fooArray @GuardedBy("lock") [] = new MyClass[3]; @GuardedBy("lock") MyClass fooArray2[] = new MyClass[3]; @GuardedBy("lock") MyClass fooArray3[][] = new MyClass[3][3]; MyClass fooArray4 @GuardedBy("lock") [][] = new MyClass[3][3]; MyClass fooArray5[] @GuardedBy("lock") [] = new MyClass[3][3]; class myClass { int i = 0; } @GuardedBy("lock") myClass myClassInstance = new myClass(); @GuardedBy("lock") Exception exception = new Exception(); class MyParametrizedType<T> { T foo; int l; } @GuardedBy("lock") MyParametrizedType<MyClass> myParametrizedType = new MyParametrizedType<MyClass>(); MyClass getFooWithWrongReturnType() { //:: error: (return.type.incompatible) return foo; // return of guarded object } @GuardedBy("lock") MyClass getFoo() { return foo; } MyClass @GuardedBy("lock") [] getFooArray() { return fooArray; } @GuardedBy("lock") MyClass[] getFooArray2() { return fooArray2; } @GuardedBy("lock") MyClass[][] getFooArray3() { return fooArray3; } MyClass @GuardedBy("lock") [][] getFooArray4() { return fooArray4; } MyClass[] @GuardedBy("lock") [] getFooArray5() { return fooArray5; } enum myEnumType { ABC, DEF } @GuardedBy("lock") myEnumType myEnum; void testEnumType() { // TODO: assignment.type.incompatible is technically correct, but we could // make it friendlier for the user if constant enum values on the RHS // automatically cast to the @GuardedBy annotation of the LHS. //:: error: (assignment.type.incompatible) myEnum = myEnumType.ABC; } final Object intrinsicLock = new Object(); void testThreadHoldsLock(@GuardedBy("intrinsicLock") MyClass m) { if (Thread.holdsLock(intrinsicLock)) { m.field.toString(); } else { //:: error: (lock.not.held) m.field.toString(); } } void testTreeTypes() { int i, l; MyClass o = new MyClass(); MyClass f = new MyClass(); // The following test cases were inspired from annotator.find.ASTPathCriterion.isSatisfiedBy in the Annotation File Utilities // TODO: File a bug for the dataflow issue mentioned in the line below. // TODO: uncomment: Hits a bug in dataflow: do { break; } while (foo.field != null); // access to guarded object in while condition of do/while loop //:: error: (lock.not.held) for (foo = new MyClass(); foo.field != null; foo = new MyClass()) { break; } // access to guarded object in condition of for loop // assignment to guarded object (OK) --- foo is still refined to @GuardedBy("lock") after this point, though. foo = new MyClass(); // A simple method call to a guarded object is not considered a dereference (only field accesses are considered dereferences). unguardedFoo.method2(); // Same as above, but the guard must be satisfied if the receiver is @GuardSatisfied. //:: error: (lock.not.held) foo.method2(); // attempt to use guarded object in a switch statement //:: error: (lock.not.held) switch (foo.field.hashCode()) { } // attempt to use guarded object inside a try with resources // try(foo = new MyClass()) { foo.field.toString(); } // Retrieving an element from a guarded array is a dereference //:: error: (lock.not.held) MyClass m = fooArray[0]; // method call on dereference of unguarded element of *guarded* array //:: error: (lock.not.held) fooArray[0].field.toString(); //:: error: (lock.not.held) l = fooArray.length; // dereference of guarded array itself // method call on dereference of guarded array element //:: error: (lock.not.held) fooArray2[0].field.toString(); // method call on dereference of unguarded array - TODO: currently preconditions are not retrieved correctly from array types. This is not unique to the Lock Checker. // fooArray2.field.toString(); // method call on dereference of guarded array element of multidimensional array //:: error: (lock.not.held) fooArray3[0][0].field.toString(); // method call on dereference of unguarded single-dimensional array element of unguarded multidimensional array - TODO: currently preconditions are not retrieved correctly from array types. This is not unique to the Lock Checker. // fooArray3[0].field.toString(); // method call on dereference of unguarded multidimensional array - TODO: currently preconditions are not retrieved correctly from array types. This is not unique to the Lock Checker. // fooArray3.field.toString(); // method call on dereference of unguarded array element of *guarded* multidimensional array //:: error: (lock.not.held) fooArray4[0][0].field.toString(); // dereference of unguarded single-dimensional array element of *guarded* multidimensional array //:: error: (lock.not.held) l = fooArray4[0].length; // dereference of guarded multidimensional array //:: error: (lock.not.held) l = fooArray4.length; // method call on dereference of unguarded array element of *guarded subarray* of multidimensional array //:: error: (lock.not.held) fooArray5[0][0].field.toString(); // dereference of guarded single-dimensional array element of multidimensional array //:: error: (lock.not.held) l = fooArray5[0].length; // dereference of unguarded multidimensional array l = fooArray5.length; //:: error: (lock.not.held) l = getFooArray().length; // dereference of guarded array returned by a method // method call on dereference of guarded array element returned by a method //:: error: (lock.not.held) getFooArray2()[0].field.toString(); // dereference of unguarded array returned by a method l = getFooArray2().length; // method call on dereference of guarded array element of multidimensional array returned by a method //:: error: (lock.not.held) getFooArray3()[0][0].field.toString(); // dereference of unguarded single-dimensional array element of multidimensional array returned by a method l = getFooArray3()[0].length; // dereference of unguarded multidimensional array returned by a method l = getFooArray3().length; // method call on dereference of unguarded array element of *guarded* multidimensional array returned by a method //:: error: (lock.not.held) getFooArray4()[0][0].field.toString(); // dereference of unguarded single-dimensional array element of *guarded* multidimensional array returned by a method //:: error: (lock.not.held) l = getFooArray4()[0].length; // dereference of guarded multidimensional array returned by a method //:: error: (lock.not.held) l = getFooArray4().length; // method call on dereference of unguarded array element of *guarded subarray* of multidimensional array returned by a method //:: error: (lock.not.held) getFooArray5()[0][0].field.toString(); // dereference of guarded single-dimensional array element of multidimensional array returned by a method //:: error: (lock.not.held) l = getFooArray5()[0].length; // dereference of unguarded multidimensional array returned by a method l = getFooArray5().length; // Test different @GuardedBy(...) present on the element and array locations. @GuardedBy("lock") MyClass @GuardedBy("lock2") [] array = new MyClass[3]; //:: error: (lock.not.held) array[0].field = new Object(); if (lock.isHeldByCurrentThread()) { //:: error: (lock.not.held) array[0].field = new Object(); if (lock2.isHeldByCurrentThread()) { array[0].field = new Object(); } } // method call on guarded object within parenthesized expression //:: error: (lock.not.held) String s = (foo.field.toString()); //:: error: (lock.not.held) foo.field.toString(); // method call on guarded object //:: error: (lock.not.held) getFoo().field.toString(); // method call on guarded object returned by a method //:: error: (lock.not.held) this.foo.field.toString(); // method call on guarded object using 'this' literal // dereference of guarded object in labeled statement label: //:: error: (lock.not.held) foo.field.toString(); // access to guarded object in instanceof expression (OK) if (foo instanceof MyClass) {} // access to guarded object in while condition of while loop (OK) while (foo != null) { break; } // binary operator on guarded object in else if condition (OK) if (false) { } else if (foo == o) { } // access to guarded object in a lambda expression Runnable rn = () -> { //:: error: (lock.not.held) foo.field.toString(); }; //:: error: (lock.not.held) i = myClassInstance.i; // access to member field of guarded object // MemberReferenceTrees? how do they work fooArray = new MyClass[3]; // second allocation of guarded array (OK) // dereference of guarded object in conditional expression tree //:: error: (lock.not.held) s = i == 5 ? foo.field.toString() : f.field.toString(); // dereference of guarded object in conditional expression tree //:: error: (lock.not.held) s = i == 5 ? f.field.toString() : foo.field.toString(); // Testing of 'return' is done in getFooWithWrongReturnType() // throwing a guarded object - when throwing an exception, it must be @GuardedBy({}). Even @GuardedByUnknown is not allowed. try { //:: error: (throw.type.invalid) throw exception; } catch (Exception e) { } // casting of a guarded object to an unguarded object //:: error: (assignment.type.incompatible) @GuardedBy({}) Object e1 = (Object) exception; // OK, since the local variable's type gets refined to @GuardedBy("lock") Object e2 = (Object) exception; //:: error: (lock.not.held) l = myParametrizedType.l; // dereference of guarded object having a parameterized type // We need to support locking on local variables and protecting local variables because these locals may contain references to fields. Somehow we need to pass along the information of which field it was. if (foo == o) { // binary operator on guarded object (OK) o.field.toString(); } if (foo == null) { // With -AconcurrentSemantics turned off, a cannot.dereference error would be expected, since // there is an attempt to dereference an expression whose type has been refined to @GuardedByBottom // (due to the comparison to null). However, with -AconcurrentSemantics turned on, foo may no longer // be null by now, the refinement to @GuardedByBottom is lost and the refined type of foo is now the // declared type ( @GuardedBy("lock") ), resulting in the lock.not.held error. //:: error: (lock.not.held) foo.field.toString(); } // TODO: Reenable: // @PolyGuardedBy should not be written here, but it is not explicitly forbidden by the framework. // @PolyGuardedBy MyClass m2 = new MyClass(); // (cannot.dereference) // m2.field.toString(); } @MayReleaseLocks public void testLocals() { final ReentrantLock localLock = new ReentrantLock(); @GuardedBy("localLock") MyClass guardedByLocalLock = new MyClass(); //:: error: (lock.not.held) guardedByLocalLock.field.toString(); @GuardedBy("lock") MyClass local = new MyClass(); //:: error: (lock.not.held) local.field.toString(); lockTheLock(); local.field.toString(); // No warning output unlockTheLock(); } @MayReleaseLocks public void testMethodAnnotations() { Random r = new Random(); if (r.nextBoolean()) { lockTheLock(); requiresLockHeldMethod(); } else { //:: error: (contracts.precondition.not.satisfied) requiresLockHeldMethod(); } if (r.nextBoolean()) { lockTheLock(); foo.field.toString(); unlockTheLock(); //:: error: (lock.not.held) foo.field.toString(); } else { //:: error: (lock.not.held) foo.field.toString(); } if (tryToLockTheLock()) { foo.field.toString(); } else { //:: error: (lock.not.held) foo.field.toString(); } if (r.nextBoolean()) { lockTheLock(); sideEffectFreeMethod(); foo.field.toString(); } else { lockTheLock(); nonSideEffectFreeMethod(); //:: error: (lock.not.held) foo.field.toString(); } if (r.nextBoolean()) { lockTheLock(); lockingFreeMethod(); foo.field.toString(); } else { lockTheLock(); nonSideEffectFreeMethod(); //:: error: (lock.not.held) foo.field.toString(); } } void methodThatTakesAnInteger(Integer i) {} void testBoxedPrimitiveType() { Integer i = null; if (i == null) {} methodThatTakesAnInteger(i); } void testReceiverGuardedByItself(@GuardedBy("<self>") TestTreeKinds this) { //:: error: (lock.not.held) method(); synchronized (this) { method(); } } void method(@GuardSatisfied TestTreeKinds this) {} void testOtherClassReceiverGuardedByItself(final @GuardedBy("<self>") OtherClass o) { //:: error: (lock.not.held) o.foo(); synchronized (o) { o.foo(); } } class OtherClass { void foo(@GuardSatisfied OtherClass this) {} } void testExplicitLockSynchronized() { final ReentrantLock lock = new ReentrantLock(); //:: error: (explicit.lock.synchronized) synchronized (lock) { } } void testPrimitiveTypeGuardedby() { //:: error: (immutable.type.guardedby) @GuardedBy("lock") int a = 0; //:: error: (immutable.type.guardedby) @GuardedBy int b = 0; //:: error: (immutable.type.guardedby) :: error: (guardsatisfied.location.disallowed) @GuardSatisfied int c = 0; //:: error: (immutable.type.guardedby) :: error: (guardsatisfied.location.disallowed) @GuardSatisfied(1) int d = 0; int e = 0; //:: error: (immutable.type.guardedby) @GuardedByUnknown int f = 0; //:: error: (immutable.type.guardedby) :: error: (assignment.type.incompatible) @GuardedByBottom int g = 0; } void testBinaryOperatorBooleanResultIsAlwaysGuardedByNothing() { @GuardedBy("lock") Object o1 = new Object(); Object o2 = new Object(); // boolean variables are implicitly @GuardedBy({}). boolean b1 = o1 == o2; boolean b2 = o2 == o1; boolean b3 = o1 != o2; boolean b4 = o2 != o1; boolean b5 = o1 instanceof Object; boolean b6 = o2 instanceof Object; boolean b7 = o1 instanceof @GuardedBy("lock") Object; boolean b8 = o2 instanceof @GuardedBy("lock") Object; } }