detekt icon indicating copy to clipboard operation
detekt copied to clipboard

SwallowedException false positive when extension function is used

Open MartinRajniak opened this issue 3 years ago • 8 comments

Context

I want to transform one exception into another before rethrowing.

For that I am using extension function like this:

    fun IOException.toMyException(): MyException = MyException(this)

Usage looks like this

    try {
        // ...
    } catch(e: IOException) {
        throw e.toMyException()
    }

But I am getting SwallowedException error for that one. Based on the documentation I assume this is not supported https://detekt.github.io/detekt/exceptions.html#compliant-code-4

Expected Behavior of the rule

I would expect this rule to ignore extensions that don't swallow the original exception.

MartinRajniak avatar Jan 24 '22 19:01 MartinRajniak

You are right. throw e.toMyException()should be considered equivalent to throw toMyException(e) but it is not. Thank you for reporting this. Would you like to look into the issue yourself and provide a pull request?

marschwar avatar Jan 24 '22 21:01 marschwar

Yes, I would love to - I just need some hints.

Currently, I think it is flagged because of this use case

    try {
        // ...
    } catch(e: IOException) {
        throw MyException(e.message) // e is swallowed
    }

I don't have much experience here, so can't tell whether it is possible to differentiate between calling functions from a class vs. extension functions.

MartinRajniak avatar Jan 24 '22 21:01 MartinRajniak

You might be right. Since there really is no way of knowing if the extension function swallows the exception or not this may not be possible at all. The extension function could be:

    fun IOException.toMyException(): MyException = MyException(this)
   // or
    fun IOException.toMyException(): MyException = MyException(this.message)

Maybe someone else has an idea on how to address this.

marschwar avatar Jan 24 '22 21:01 marschwar

Actually, I don't think that this is checked once an exception is passed to the function.

    try {
        // ...
    } catch(e: IOException) {
        throw MyException(e)
    }
    try {
        // ...
    } catch(e: IOException) {
        println(e) // logging is ok here
    }

both MyException constructor and println function can swallow the exception (I have tried swallowing it in the body and it works - no flagging)

I think the check is only for immediate handling and the condition might be:

  • either exception is not used (passed into another function),
  • or it is used but a function is invoked on the exception (to avoid .toString())

MartinRajniak avatar Jan 24 '22 21:01 MartinRajniak

Looking at the tests for the rule it looks to me that you either do something with the exception (without rethrowing) or you throw an exception using the exception as a parameter. There could be more to it though.

You could also "game the system" by doing

    try {
      // ...
    } catch (e: IOException) {
        e.throwMyException()
    }

private fun IOException.throwMyException(): Nothing = throw MyException(this)

Though I am not sure I really like this "solution".

marschwar avatar Jan 24 '22 21:01 marschwar

This issue is stale because it has been open 90 days with no activity. Please comment or this will be closed in 7 days.

github-actions[bot] avatar Apr 25 '22 02:04 github-actions[bot]

May I try to apply a simple fix for the issue? Or is it necessary to come up with a general solution (which I don't have obviously)?

VitalyVPinchuk avatar Jul 06 '22 13:07 VitalyVPinchuk

Go ahead!

BraisGabin avatar Jul 06 '22 15:07 BraisGabin

@BraisGabin Should it closed too?

VitalyVPinchuk avatar Sep 11 '22 08:09 VitalyVPinchuk

Closing. The reason is at #5046

BraisGabin avatar Sep 11 '22 08:09 BraisGabin

I ran into this twice in a year now. My utility is

fun Throwable.withRootCause(cause: Throwable): Throwable {
	require(cause !in generateSequence(this) { it.cause })
	generateSequence(this) { it.cause }.last().initCause(cause)
	return this
}

it adds extra information to the throwable:

internal fun assertOutput(command: List<Any>, expected: String, message: String? = null) {
	try {
		val output = command.map(Any?::toString).runCommand()
		assertEquals(expected.normalize(), output.normalize(), message)
	} catch (@Suppress("SwallowedException") ex: Throwable) { // Detekt doesn't see into the extension fun.
		throw ex.withRootCause(IllegalArgumentException("Command: $command"))
	}
}

I see how complex "whitelisting" is. I think https://github.com/detekt/detekt/issues/5510 might be the solution, the problem is that it might need some framework changes, because the violation is reported on the catch variable, but on the withRootCause call. For this reason I think it might need special logic in the rule, OR the framework to consider additional locations in the suppression process.

Note: luckily I could rewrite this to .apply { setRootCause(...) } which looks ugly, but should make detekt pass, but as soon as I need to reconstruct the throwable (because stack trace manipulation or other magic, this falls apart, so not a general solution.

TWiStErRob avatar Apr 11 '23 14:04 TWiStErRob