diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c5ded4e22dbf..c5bc664b4e6d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2436,8 +2436,35 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val capabilityProof = caughtExceptions.reduce(OrType(_, _, true)) untpd.Block(makeCanThrow(capabilityProof), expr) + /** Graphic explanation of NotNullInfo logic: + * Leftward exit indicates exceptional case + * Downward exit indicates normal case + * + * ┌─────┐ + * │ Try ├─────┬────────┬─────┐ + * └──┬──┘ ▼ ▼ │ + * │ ┌───────┐┌───────┐ │ + * │ │ Catch ││ Catch ├─┤ + * │ └───┬───┘└───┬───┘ │ + * └─┬──────┴────────┘ │ + * ▼ ▼ + * ┌─────────┐ ┌─────────┐ + * │ Finally ├──────┐ │ Finally ├──┐ + * └────┬────┘ │ └────┬────┘ │ + * ▼ └─────────┴───────┴─► + * exprNNInfo = Effect of the try block if completed normally + * casesNNInfo = Effect of catch blocks completing normally + * normalAfterCasesInfo = Exceptional try followed by normal catches + * We type finalizer with normalAfterCasesInfo.retracted + * + * Overall effect of try-catch-finally = + * resNNInfo = + * (exprNNInfo OR normalAfterCasesInfo) followed by normal finally block + * + * For all nninfo, if a tree can be typed using nninfo.retractedInfo, then it can + * also be typed using nninfo. + */ def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = - var nnInfo = NotNullInfo.empty val expr2 :: cases2x = harmonic(harmonize, pt) { // We want to type check tree.expr first to comput NotNullInfo, but `addCanThrowCapabilities` // uses the types of patterns in `tree.cases` to determine the capabilities. @@ -2450,25 +2477,23 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val casesEmptyBody2 = typedCases(casesEmptyBody1, EmptyTree, defn.ThrowableType, WildcardType) val expr1 = typed(addCanThrowCapabilities(tree.expr, casesEmptyBody2), pt.dropIfProto) - // Since we don't know at which point the the exception is thrown in the body, - // we have to collect any reference that is once retracted. - nnInfo = expr1.notNullInfo.retractedInfo - - val casesCtx = ctx.addNotNullInfo(nnInfo) + val casesCtx = ctx.addNotNullInfo(expr1.notNullInfo.retractedInfo) val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)(using casesCtx) expr1 :: cases1 }: @unchecked val cases2 = cases2x.asInstanceOf[List[CaseDef]] + val exprNNInfo = expr2.notNullInfo + val casesNNInfo = + cases2.map(_.notNullInfo) + .foldLeft(NotNullInfo.empty.terminatedInfo)(_.alt(_)) + val normalAfterCasesInfo = exprNNInfo.retractedInfo.seq(casesNNInfo) // It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught. // Therefore, the code in the finalizer and after the try block can only rely on the retracted // info from the cases' body. - if cases2.nonEmpty then - nnInfo = nnInfo.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_))) - - val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nnInfo)) - nnInfo = nnInfo.seq(finalizer1.notNullInfo) - assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nnInfo) + val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(normalAfterCasesInfo.retractedInfo)) + val resNNInfo = exprNNInfo.alt(normalAfterCasesInfo).seq(finalizer1.notNullInfo) + assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(resNNInfo) def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try = val cases: List[untpd.CaseDef] = tree.handler match diff --git a/tests/explicit-nulls/neg/i21619.scala b/tests/explicit-nulls/neg/i21619.scala index d7af27e3fe64..07e61e636a31 100644 --- a/tests/explicit-nulls/neg/i21619.scala +++ b/tests/explicit-nulls/neg/i21619.scala @@ -1,3 +1,5 @@ +class SomeException extends Exception + def test1: String = var x: String | Null = null x = "" @@ -13,6 +15,22 @@ def test1: String = x.replace("", "") // error def test2: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case e: NoSuchMethodError => + x = "e" + x.replace("", "") // ok + +// From i24296 +def test2_2: String = var x: String | Null = null x = "" var i: Int = 1 @@ -25,8 +43,43 @@ def test2: String = catch case e: Exception => x = "e" + case _ => x.replace("", "") // error +def test2_3: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case e: NoSuchMethodError => + x = "e" + case e: AbstractMethodError => + x = "e" + x.replace("", "") // ok + +def test2_4: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case e: NoSuchMethodError => + x = "e" + case e: AbstractMethodError => + throw new Exception() + x.replace("", "") // ok + def test3: String = var x: String | Null = null x = "" @@ -89,4 +142,225 @@ def test6 = { } } x.replace("", "") // error -} \ No newline at end of file +} + +// From i24296 +def test7() = + var x: String | Null = null + try { + x = "" + } catch { + case e => + throw e + } + x.trim() // ok + +def test8() = + var x: String | Null = null + try { + try { + x = "" + } catch { + case e => throw e + } + } catch { + case e => throw e + } + x.trim() // ok + +def test9() = + var x: String | Null = null + try { + x = "" + } catch { + case e: AssertionError => + throw e + case _ => + } + x.trim() // error + +def test9_2() = + var x: String | Null = null + try { + x = "" + } catch { + case e: AssertionError => + throw e + } + x.trim() // ok + +def test10() = + var x: String | Null = null + try { + x = "" + } catch { + case e => + throw e + } finally { + x = null + } + x.trim() // error + +def test11() = + var x: String | Null = null + try { + x = "" + } catch { + case e => + x = null + throw e + } finally { + x = "" + } + x.trim() // ok + +def test12() = + var x: String | Null = null + try { + x = "" + } catch { + case e => + x = null + throw e + } finally { + throw new Exception + x = "" + } + x.trim() // ok + +def test12_2() = + var x: String | Null = null + try { + x = "" + } catch { + case e => + x = null + throw e + } finally { + throw new Exception + } + x.trim() // ok + +def test13() = + var x: String | Null = null + try { + x = null + throw new RuntimeException + } finally { + x.trim() // error + } + x.trim() // OK + +def test14() = + var x: String | Null = "" + x = "" + try { + try { + } catch { + case e => + x = null + throw e + } + } catch { + case e => + x.trim() // error + } + + + +def test15: String = + var x: String | Null = ??? + var y: String | Null = ??? + // ... + try + x = null + // ... + x = "" + catch + case e: SomeException => + x = null + // situation 1: don't throw or return + // situation 2: + return "" + finally + y = x + // should always error on y.trim + + y.trim // error (ideally, should error if situation 1, should not error if situation 2) + +def test16: String = + var x: String | Null = ??? + x = "" + try + // call some method that throws + // ... + x = "" + catch + case e: SomeException => + x = null + // call some method that throws + // ... + x = "" + finally {} + + x.trim() // ok + +def test17: String = + var x: String | Null = ??? + x = "" + try + // call some method that throws + // ... + x = "" + catch + case e: SomeException => + x = null + // call some method that throws + // ... + x = "" + finally + println(x.trim()) // error + + "" + +def test18: String = + var x: String | Null = ??? + var y: String | Null = ??? + // ... + try + x = null + y = null + // ... + x = "" + y = "" + catch + case e: SomeException => + x = null + return "" + finally {} + + x.trim + y.trim + + +def test19: String = + var x: String | Null = ??? + try + x = null + catch + case e: SomeException => + x = null + throw e + finally + throw new Exception() + x.trim // ok + +def test20: String = + var x: String | Null = ??? + x = "" + try + x = "" + catch + case e: SomeException => + x = "" + finally {} + x.trim // ok \ No newline at end of file