Search code examples
kotlinintellij-idea

Issue IDE warning if annotated member is not surrounded with a particular block


I have a data structure which has members that are not thread safe and the caller needs to lock the resource for reading and writing as appropriate. Here's a minimal code sample:

class ExampleResource : LockableProjectItem {
    override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()

    @RequiresReadLock
    val nonThreadSafeMember: String = ""
}

interface LockableProjectItem {
    val readWriteLock: ReadWriteLock
}

fun <T : LockableProjectItem, Out> T.readLock(block: T.() -> Out): Out {
    try {
        readWriteLock.readLock().lock()
        return block(this)
    } finally {
        readWriteLock.readLock().unlock()
    }
}

fun <T : LockableProjectItem, Out> T.writeLock(block: T.() -> Out): Out {
    try {
        readWriteLock.writeLock().lock()
        return block(this)
    } finally {
        readWriteLock.writeLock().unlock()
    }
}

annotation class RequiresReadLock

A call ExampleResource.nonThreadSafeMember might then look like this:

val resource = ExampleResource()
val readResult = resource.readLock { nonThreadSafeMember }

To make sure that the caller is aware that the resource needs to be locked, I would like the IDE to issue a warning for any members that are annotated with @RequiresReadLock and are not surrounded with a readLock block. Is there any way to do this in IntelliJ without writing a custom plugin for the IDE?


Solution

  • I think this is sort of a hack, but using context receivers might work. I don't think they are intended to be used in this way though.

    You can declare a dummy object to act as the context receiver, and add that as a context receiver to the property:

    object ReadLock
    
    class ExampleResource : LockableProjectItem {
        override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
    
        // properties with context receivers cannot have a backing field, so we need to explicitly declare this
        private val nonThreadSafeMemberField: String = ""
    
        context(ReadLock)
        val nonThreadSafeMember: String
            get() = nonThreadSafeMemberField
    }
    

    Then in readLock, you pass the object:

    fun <T : LockableProjectItem, Out> T.readLock(block: context(ReadLock) T.() -> Out): Out {
        try {
            readWriteLock.readLock().lock()
            return block(ReadLock, this)
        } finally {
            readWriteLock.readLock().unlock()
        }
    }
    

    Notes:

    • This will give you an error if you try to access nonThreadSafeMember without the context receiver:

      val resource = ExampleResource()
      val readResult = resource.nonThreadSafeMember //error
      
    • You can still access nonThreadSafeMember without acquiring a read lock by doing e.g.

      with(ReadLock) { // with(ReadLock) doesn't acquire the lock, just gets the context receiver
          resource.nonThreadSafeMember // no error
      }
      

      But it's way harder to accidentally write something like this, which I think is what you are trying to prevent.

    • If you call another function inside readLock, and you want to access nonThreadSafeMember inside that function, you should mark that function with context(ReadLock) too. e.g.

      fun main() {
          val resource = ExampleResource()
          val readResult = resource.readLock {
              foo(this)
          }
      }
      
      context(ReadLock)
      fun foo(x: ExampleResource) {
          x.nonThreadSafeMember
      }
      

      The context receiver is propagated through.