From 31428b21497320e3e90561376a9804e6c0669224 Mon Sep 17 00:00:00 2001 From: Seyon Sivatharan Date: Fri, 30 May 2025 01:09:27 -0400 Subject: [PATCH 1/4] Modify pattern matcher to raise an error instead of generating illegal isInstanceOf[Null] and isInstanceOf[Nothing] Added a check to the pattern matcher to raise an error when Null or Nothing is used in a type pattern instead of generating isInstanceOf[Null] and isInstanceOf[Nothing] and raising an error later in the erasure phase --- .../src/dotty/tools/dotc/core/Types.scala | 26 +++++++++++++++++++ .../tools/dotc/transform/PatternMatcher.scala | 3 +++ tests/neg/i23243.check | 4 +++ tests/neg/i23243.scala | 9 +++++++ tests/neg/i23243a.scala | 8 ++++++ tests/neg/i4004.scala | 5 ---- tests/neg/i4004a.scala | 10 +++++++ 7 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/neg/i23243.check create mode 100644 tests/neg/i23243.scala create mode 100644 tests/neg/i23243a.scala create mode 100644 tests/neg/i4004a.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 728f742ea1ad..9ee4001e015c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -370,6 +370,32 @@ object Types extends TypeUtils { loop(this) } + /* Is this type exactly Nothing or is an AndOrType with a type that is exactly Nothing? */ + def hasNothing(using Context): Boolean = { + def loop(tp: Type): Boolean = tp match { + case tp: TypeRef => + tp.isExactlyNothing + case tp: AndOrType => + loop(tp.tp1) || loop(tp.tp2) + case _ => + false + } + loop(this) + } + + /* Is this type exactly Null or is an AndOrType with a type that is exactly Null? */ + def hasNull(using Context): Boolean = { + def loop(tp: Type): Boolean = tp match { + case tp: TypeRef => + tp.isExactlyNull + case tp: AndOrType => + loop(tp.tp1) || loop(tp.tp2) + case _ => + false + } + loop(this) + } + /** Is this type guaranteed not to have `null` as a value? */ final def isNotNull(using Context): Boolean = this match { case tp: ConstantType => tp.value.value != null diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index e2505144abda..e2a7df1e05f4 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -418,6 +418,9 @@ object PatternMatcher { && !hasExplicitTypeArgs(extractor) case _ => false } + + if (tpt.tpe.hasNull || tpt.tpe.hasNothing) report.error(em"${tpt.tpe} cannot be used in runtime type tests", tpt) + TestPlan(TypeTest(tpt, isTrusted), scrutinee, tree.span, letAbstract(ref(scrutinee).cast(tpt.tpe)) { casted => nonNull += casted diff --git a/tests/neg/i23243.check b/tests/neg/i23243.check new file mode 100644 index 000000000000..9a73f6ef280d --- /dev/null +++ b/tests/neg/i23243.check @@ -0,0 +1,4 @@ +-- Error: tests\neg\i23243.scala:8:20 ---------------------------------------------------------------------------------- +8 | case Extractor() => println("foo matched") // error + | ^ + | String | Null cannot be used in runtime type tests diff --git a/tests/neg/i23243.scala b/tests/neg/i23243.scala new file mode 100644 index 000000000000..9dd748da53ed --- /dev/null +++ b/tests/neg/i23243.scala @@ -0,0 +1,9 @@ +object Extractor: + def unapply(s: String|Null): Boolean = true + +class A + +def main = + ("foo": (A|String)) match + case Extractor() => println("foo matched") // error + case _ => println("foo didn't match") diff --git a/tests/neg/i23243a.scala b/tests/neg/i23243a.scala new file mode 100644 index 000000000000..c6330e49c17e --- /dev/null +++ b/tests/neg/i23243a.scala @@ -0,0 +1,8 @@ +def main = { + ("foo": String) match { + case a: (String | Nothing) => // error + println("bar") + case _ => + println("foo") + } +} diff --git a/tests/neg/i4004.scala b/tests/neg/i4004.scala index bf757a0863a7..37b55891fd83 100644 --- a/tests/neg/i4004.scala +++ b/tests/neg/i4004.scala @@ -1,13 +1,8 @@ @main def Test = - "a".isInstanceOf[Null] // error - null.isInstanceOf[Null] // error - "a".isInstanceOf[Nothing] // error - "a".isInstanceOf[Singleton] // error "a" match case _: Null => () // error case _: Nothing => () // error - case _: Singleton => () // error case _ => () null match diff --git a/tests/neg/i4004a.scala b/tests/neg/i4004a.scala new file mode 100644 index 000000000000..10201409c908 --- /dev/null +++ b/tests/neg/i4004a.scala @@ -0,0 +1,10 @@ +@main def Test = + "a".isInstanceOf[Null] // error + null.isInstanceOf[Null] // error + "a".isInstanceOf[Nothing] // error + "a".isInstanceOf[Singleton] // error + + "a" match { + case _: Singleton => () // error + case _ => () + } From 725bf5ea1d69cad81f81bdc49f5e9211c8aef20b Mon Sep 17 00:00:00 2001 From: Seyon Sivatharan Date: Sat, 31 May 2025 19:55:59 -0400 Subject: [PATCH 2/4] Revert previous changes and simplify Nothing | T to T and Null | T to if T is nullable --- .../src/dotty/tools/dotc/core/Types.scala | 26 ------------------- .../tools/dotc/transform/PatternMatcher.scala | 2 -- .../tools/dotc/transform/TypeTestsCasts.scala | 10 ++++--- 3 files changed, 7 insertions(+), 31 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 9ee4001e015c..728f742ea1ad 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -370,32 +370,6 @@ object Types extends TypeUtils { loop(this) } - /* Is this type exactly Nothing or is an AndOrType with a type that is exactly Nothing? */ - def hasNothing(using Context): Boolean = { - def loop(tp: Type): Boolean = tp match { - case tp: TypeRef => - tp.isExactlyNothing - case tp: AndOrType => - loop(tp.tp1) || loop(tp.tp2) - case _ => - false - } - loop(this) - } - - /* Is this type exactly Null or is an AndOrType with a type that is exactly Null? */ - def hasNull(using Context): Boolean = { - def loop(tp: Type): Boolean = tp match { - case tp: TypeRef => - tp.isExactlyNull - case tp: AndOrType => - loop(tp.tp1) || loop(tp.tp2) - case _ => - false - } - loop(this) - } - /** Is this type guaranteed not to have `null` as a value? */ final def isNotNull(using Context): Boolean = this match { case tp: ConstantType => tp.value.value != null diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index e2a7df1e05f4..4ad31d65550c 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -418,8 +418,6 @@ object PatternMatcher { && !hasExplicitTypeArgs(extractor) case _ => false } - - if (tpt.tpe.hasNull || tpt.tpe.hasNothing) report.error(em"${tpt.tpe} cannot be used in runtime type tests", tpt) TestPlan(TypeTest(tpt, isTrusted), scrutinee, tree.span, letAbstract(ref(scrutinee).cast(tpt.tpe)) { casted => diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index a8c8ec8ce1d8..4798683da68b 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -330,13 +330,17 @@ object TypeTestsCasts { expr.isInstance(testType).withSpan(tree.span) case OrType(tp1, tp2) => evalOnce(expr) { e => - transformTypeTest(e, tp1, flagUnrelated = false) - .or(transformTypeTest(e, tp2, flagUnrelated = false)) + lazy val tp1Tree = transformTypeTest(e, tp1, flagUnrelated = false) + lazy val tp2Tree = transformTypeTest(e, tp2, flagUnrelated = false) + + if (tp1.isNothingType || (tp1.isNullType && !tp2.isNotNull)) tp2Tree + else if (tp2.isNothingType || (tp2.isNullType && !tp1.isNotNull)) tp1Tree + else tp1Tree.or(tp2Tree) } case AndType(tp1, tp2) => evalOnce(expr) { e => transformTypeTest(e, tp1, flagUnrelated) - .and(transformTypeTest(e, tp2, flagUnrelated)) + .and(transformTypeTest(e, tp2, flagUnrelated)) } case defn.MultiArrayOf(elem, ndims) if isGenericArrayElement(elem, isScala2 = false) => def isArrayTest(arg: Tree) = From 3e25a19ff06424236d07a8a1304ccb8ca78c0e25 Mon Sep 17 00:00:00 2001 From: Seyon Sivatharan Date: Tue, 19 Aug 2025 00:33:53 -0400 Subject: [PATCH 3/4] Modify logic type tests logic to strip the | Null's from testType and add a forceStrip argument to .stripNull that makes it strip | Null's even when explicit-nulls is not enabled --- .../dotty/tools/dotc/core/NullOpsDecorator.scala | 9 +++++---- .../dotty/tools/dotc/transform/TypeTestsCasts.scala | 13 +++++-------- tests/neg/i23243.check | 4 ---- tests/{neg => pos}/i23243.scala | 4 ++-- 4 files changed, 12 insertions(+), 18 deletions(-) delete mode 100644 tests/neg/i23243.check rename tests/{neg => pos}/i23243.scala (72%) diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 291498dbc558..1d34fddf4d78 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -8,13 +8,14 @@ import Types.* object NullOpsDecorator: extension (self: Type) - /** Syntactically strips the nullability from this type. + /** If explicit-nulls is enabled, syntactically strips the nullability from this type. + * If explicit-nulls is not enabled, removes all Null in unions. * If the type is `T1 | ... | Tn`, and `Ti` references to `Null`, * then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`. * If this type isn't (syntactically) nullable, then returns the type unchanged. - * The type will not be changed if explicit-nulls is not enabled. + * The type will not be changed if explicit-nulls is not enabled and forceStrip is false. */ - def stripNull(stripFlexibleTypes: Boolean = true)(using Context): Type = { + def stripNull(stripFlexibleTypes: Boolean = true, forceStrip: Boolean = false)(using Context): Type = { def strip(tp: Type): Type = val tpWiden = tp.widenDealias val tpStripped = tpWiden match { @@ -42,7 +43,7 @@ object NullOpsDecorator: } if tpStripped ne tpWiden then tpStripped else tp - if ctx.explicitNulls then strip(self) else self + if ctx.explicitNulls || forceStrip then strip(self) else self } /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 4798683da68b..0c35b95e09bd 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -16,6 +16,7 @@ import reporting.* import config.Printers.{ transforms => debug } import patmat.Typ +import dotty.tools.dotc.core.NullOpsDecorator.stripNull import dotty.tools.dotc.util.SrcPos /** This transform normalizes type tests and type casts, @@ -323,19 +324,15 @@ object TypeTestsCasts { * The transform happens before erasure of `testType`, thus cannot be merged * with `transformIsInstanceOf`, which depends on erased type of `testType`. */ - def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealias match { + def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealias.stripNull(false, true) match { case tref: TermRef if tref.symbol == defn.EmptyTupleModule => ref(defn.RuntimeTuples_isInstanceOfEmptyTuple).appliedTo(expr) case _: SingletonType => expr.isInstance(testType).withSpan(tree.span) - case OrType(tp1, tp2) => + case t @ OrType(tp1, tp2) => evalOnce(expr) { e => - lazy val tp1Tree = transformTypeTest(e, tp1, flagUnrelated = false) - lazy val tp2Tree = transformTypeTest(e, tp2, flagUnrelated = false) - - if (tp1.isNothingType || (tp1.isNullType && !tp2.isNotNull)) tp2Tree - else if (tp2.isNothingType || (tp2.isNullType && !tp1.isNotNull)) tp1Tree - else tp1Tree.or(tp2Tree) + transformTypeTest(e, tp1, flagUnrelated = false) + .or(transformTypeTest(e, tp2, flagUnrelated = false)) } case AndType(tp1, tp2) => evalOnce(expr) { e => diff --git a/tests/neg/i23243.check b/tests/neg/i23243.check deleted file mode 100644 index 9a73f6ef280d..000000000000 --- a/tests/neg/i23243.check +++ /dev/null @@ -1,4 +0,0 @@ --- Error: tests\neg\i23243.scala:8:20 ---------------------------------------------------------------------------------- -8 | case Extractor() => println("foo matched") // error - | ^ - | String | Null cannot be used in runtime type tests diff --git a/tests/neg/i23243.scala b/tests/pos/i23243.scala similarity index 72% rename from tests/neg/i23243.scala rename to tests/pos/i23243.scala index 9dd748da53ed..9a6205f9ff72 100644 --- a/tests/neg/i23243.scala +++ b/tests/pos/i23243.scala @@ -1,9 +1,9 @@ object Extractor: def unapply(s: String|Null): Boolean = true - + class A def main = ("foo": (A|String)) match - case Extractor() => println("foo matched") // error + case Extractor() => println("foo matched") case _ => println("foo didn't match") From 558d67f24c26400d7e3385765764ed3ba3566cab Mon Sep 17 00:00:00 2001 From: Seyon Sivatharan Date: Fri, 29 Aug 2025 16:32:38 -0400 Subject: [PATCH 4/4] Add warning for having | Null's in patterns in pattern matching --- .../tools/dotc/core/NullOpsDecorator.scala | 6 ++-- .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 6 ++++ .../tools/dotc/transform/TypeTestsCasts.scala | 6 ++-- .../tools/dotc/transform/patmat/Space.scala | 34 +++++++++++++++++-- .../dotty/tools/dotc/typer/Nullables.scala | 4 +-- tests/explicit-nulls/warn/i23243.scala | 18 ++++++++++ tests/pos/i23243.scala | 9 ----- tests/warn/i23243.check | 20 +++++++++++ tests/warn/i23243.scala | 19 +++++++++++ 10 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 tests/explicit-nulls/warn/i23243.scala delete mode 100644 tests/pos/i23243.scala create mode 100644 tests/warn/i23243.check create mode 100644 tests/warn/i23243.scala diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 1d34fddf4d78..47427f58e591 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -9,7 +9,7 @@ object NullOpsDecorator: extension (self: Type) /** If explicit-nulls is enabled, syntactically strips the nullability from this type. - * If explicit-nulls is not enabled, removes all Null in unions. + * If explicit-nulls is not enabled and forceStrip is enabled, removes all Null in unions. * If the type is `T1 | ... | Tn`, and `Ti` references to `Null`, * then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`. * If this type isn't (syntactically) nullable, then returns the type unchanged. @@ -47,8 +47,8 @@ object NullOpsDecorator: } /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ - def isNullableUnion(using Context): Boolean = { - val stripped = self.stripNull() + def isNullableUnion(stripFlexibleTypes: Boolean = true, forceStrip: Boolean = false)(using Context): Boolean = { + val stripped = self.stripNull(stripFlexibleTypes, forceStrip) stripped ne self } end extension diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 103687abdbff..040a742e0094 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -235,6 +235,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case CannotInstantiateQuotedTypeVarID // errorNumber: 219 case DefaultShadowsGivenID // errorNumber: 220 case RecurseWithDefaultID // errorNumber: 221 + case MatchCaseUnnecessaryOrNullID // errorNumber: 222 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index d3f323fa3099..b802c90d758f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -962,6 +962,12 @@ extends PatternMatchMsg(MatchCaseOnlyNullWarningID) { def explain(using Context) = "" } +class MatchCaseUnnecessaryNullable(tp: Type)(using Context) +extends PatternMatchMsg(MatchCaseUnnecessaryOrNullID) { + def msg(using Context) = i"""$tp is nullable, but will not be matched by ${hl("null")}.""" + def explain(using Context) = "" +} + class MatchableWarning(tp: Type, pattern: Boolean)(using Context) extends TypeMsg(MatchableWarningID) { def msg(using Context) = diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 0c35b95e09bd..4ed5cb988d69 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -324,12 +324,12 @@ object TypeTestsCasts { * The transform happens before erasure of `testType`, thus cannot be merged * with `transformIsInstanceOf`, which depends on erased type of `testType`. */ - def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealias.stripNull(false, true) match { + def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealias.stripNull(stripFlexibleTypes = false, forceStrip = true) match { case tref: TermRef if tref.symbol == defn.EmptyTupleModule => ref(defn.RuntimeTuples_isInstanceOfEmptyTuple).appliedTo(expr) case _: SingletonType => expr.isInstance(testType).withSpan(tree.span) - case t @ OrType(tp1, tp2) => + case OrType(tp1, tp2) => evalOnce(expr) { e => transformTypeTest(e, tp1, flagUnrelated = false) .or(transformTypeTest(e, tp2, flagUnrelated = false)) @@ -337,7 +337,7 @@ object TypeTestsCasts { case AndType(tp1, tp2) => evalOnce(expr) { e => transformTypeTest(e, tp1, flagUnrelated) - .and(transformTypeTest(e, tp2, flagUnrelated)) + .and(transformTypeTest(e, tp2, flagUnrelated)) } case defn.MultiArrayOf(elem, ndims) if isGenericArrayElement(elem, isScala2 = false) => def isArrayTest(arg: Tree) = diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index b7e1f349a377..cbc1895ffa83 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -925,9 +925,39 @@ object SpaceEngine { @tailrec def recur(cases: List[CaseDef], prevs: List[Space], deferred: List[Tree]): Unit = cases match case Nil => - case CaseDef(pat, guard, _) :: rest => + case (c @ CaseDef(pat, guard, _)) :: rest => + def checkForUnnecessaryNullable(p: Tree): Unit = p match { + case Bind(_, body) => checkForUnnecessaryNullable(body) + case Typed(expr, tpt) => + if tpt.tpe.isNullableUnion(stripFlexibleTypes = false, forceStrip = true) then + report.warning(MatchCaseUnnecessaryNullable(tpt.tpe), p.srcPos) + else + checkForUnnecessaryNullable(expr) + case UnApply(_, _, patterns) => + patterns.map(checkForUnnecessaryNullable) + case Alternative(patterns) => patterns.map(checkForUnnecessaryNullable) + case _ => + } + + def handlesNull(p: Tree): Boolean = p match { + case Literal(Constant(null)) => true + case Typed(expr, tpt) => tpt.tpe.isSingleton || handlesNull(expr) // assume all SingletonType's handle null (see redundant-null.scala) + case Bind(_, body) => handlesNull(body) + case Alternative(patterns) => patterns.exists(handlesNull) + case _ => false + } val curr = trace(i"project($pat)")(projectPat(pat)) - val covered = trace("covered")(simplify(intersect(curr, targetSpace))) + var covered = trace("covered")(simplify(intersect(curr, targetSpace))) + + checkForUnnecessaryNullable(pat) + + if !handlesNull(pat) && !isWildcardArg(pat) then + // Remove nullSpace from covered only if: + // 1. No pattern is the null constant (e.g., `case null =>` or `case ... | null | ... =>`) or + // has a SingletonType (e.g., `case _: n.type =>` where `val n = null`, see redundant-null.scala) in one of its pattern(s) + // 2. The pattern is a wildcard pattern. + covered = minus(covered, nullSpace) + val prev = trace("prev")(simplify(Or(prevs))) if prev == Empty && covered == Empty then // defer until a case is reachable recur(rest, prevs, pat :: deferred) diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 609dad894b6c..50d35ab6a1f2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -433,10 +433,10 @@ object Nullables: def computeAssignNullable()(using Context): tree.type = var nnInfo = tree.rhs.notNullInfo tree.lhs match - case TrackedRef(ref) if ctx.explicitNulls && ref.isNullableUnion => + case TrackedRef(ref) if ctx.explicitNulls && ref.isNullableUnion() => nnInfo = nnInfo.seq: val rhstp = tree.rhs.typeOpt - if rhstp.isNullType || rhstp.isNullableUnion then + if rhstp.isNullType || rhstp.isNullableUnion() then // If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the // lhs variable is no longer trackable. We don't need to check whether the type `T` // is correct here, as typer will check it. diff --git a/tests/explicit-nulls/warn/i23243.scala b/tests/explicit-nulls/warn/i23243.scala new file mode 100644 index 000000000000..5ff05a17c8a0 --- /dev/null +++ b/tests/explicit-nulls/warn/i23243.scala @@ -0,0 +1,18 @@ +object Extractor: + def unapply(s: String|Null): Boolean = true + +class A + +def main = + ("foo": (A|String)) match + case Extractor() => println("foo matched") // warn: String | Null is nullable + case _ => println("foo didn't match") + val s: Some[Any] = Some(5) + + s match { + case Some(s: (String | Null)) => // warn: String | Null is nullable + case Some(Some(s: (String | Null))) => // warn: String | Null is nullable + case _: s.type => + case Some(s: Int) => + case _ => + } diff --git a/tests/pos/i23243.scala b/tests/pos/i23243.scala deleted file mode 100644 index 9a6205f9ff72..000000000000 --- a/tests/pos/i23243.scala +++ /dev/null @@ -1,9 +0,0 @@ -object Extractor: - def unapply(s: String|Null): Boolean = true - -class A - -def main = - ("foo": (A|String)) match - case Extractor() => println("foo matched") - case _ => println("foo didn't match") diff --git a/tests/warn/i23243.check b/tests/warn/i23243.check new file mode 100644 index 000000000000..5cbc8538a410 --- /dev/null +++ b/tests/warn/i23243.check @@ -0,0 +1,20 @@ +-- [E222] Pattern Match Warning: tests/warn/i23243.scala:8:9 ----------------------------------------------------------- +8 | case Extractor() => println("foo matched") // warn: String | Null is nullable + | ^^^^^^^^^^^ + | String | Null is nullable, but will not be matched by null. +-- [E222] Pattern Match Warning: tests/warn/i23243.scala:13:17 --------------------------------------------------------- +13 | case Some(s: (String | Null)) => // warn: String | Null is nullable + | ^^^^^^^^^^^^^^^ + | String | Null is nullable, but will not be matched by null. +-- [E222] Pattern Match Warning: tests/warn/i23243.scala:14:22 --------------------------------------------------------- +14 | case Some(Some(s: (String | Null))) => // warn: String | Null is nullable + | ^^^^^^^^^^^^^^^ + | String | Null is nullable, but will not be matched by null. +-- [E222] Pattern Match Warning: tests/warn/i23243.scala:17:22 --------------------------------------------------------- +17 | case (null | Some(_: (Int | Null)))| Some(_: (String | Null)) => // warn: Int | Null is nullable // warn: String | Null is nullable + | ^^^^^^^^^^^^^^^ + | Int | Null is nullable, but will not be matched by null. +-- [E222] Pattern Match Warning: tests/warn/i23243.scala:17:46 --------------------------------------------------------- +17 | case (null | Some(_: (Int | Null)))| Some(_: (String | Null)) => // warn: Int | Null is nullable // warn: String | Null is nullable + | ^^^^^^^^^^^^^^^^^^ + | String | Null is nullable, but will not be matched by null. diff --git a/tests/warn/i23243.scala b/tests/warn/i23243.scala new file mode 100644 index 000000000000..21cadd057b8e --- /dev/null +++ b/tests/warn/i23243.scala @@ -0,0 +1,19 @@ +object Extractor: + def unapply(s: String|Null): Boolean = true + +class A + +def main = + ("foo": (A|String)) match + case Extractor() => println("foo matched") // warn: String | Null is nullable + case _ => println("foo didn't match") + val s: Any = 5 + + s match { + case Some(s: (String | Null)) => // warn: String | Null is nullable + case Some(Some(s: (String | Null))) => // warn: String | Null is nullable + case Some(null) => + case _: s.type => + case (null | Some(_: (Int | Null)))| Some(_: (String | Null)) => // warn: Int | Null is nullable // warn: String | Null is nullable + case _ => + }