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

Time:11-15

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?

CodePudding user response:

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.

  • Related