diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java b/compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java index cc67c76c33b9..e580288316b5 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java @@ -116,6 +116,7 @@ public enum ErrorMessageID { TraitRedefinedFinalMethodFromAnyRefID, PackageNameAlreadyDefinedID, UnapplyInvalidNumberOfArgumentsID, + UnapplyInvalidReturnTypeID, StaticFieldsOnlyAllowedInObjectsID, CyclicInheritanceID, BadSymbolicReferenceID, diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index 6cbc0526b9d6..aea289b4e1b3 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -25,6 +25,7 @@ import dotty.tools.dotc.core.Flags._ import dotty.tools.dotc.core.SymDenotations.SymDenotation import dotty.tools.dotc.typer.ErrorReporting.Errors import scala.util.control.NonFatal +import StdNames.nme object messages { @@ -1919,6 +1920,61 @@ object messages { |""".stripMargin } + case class UnapplyInvalidReturnType(unapplyResult: Type, unapplyName: Symbol#ThisName)(implicit ctx: Context) + extends Message(UnapplyInvalidReturnTypeID) { + val kind = "Type Mismatch" + val addendum = + if (ctx.scala2Mode && unapplyName == nme.unapplySeq) + "\nYou might want to try to rewrite the extractor to use `unapply` instead." + else "" + val msg = hl"""| ${Red(i"$unapplyResult")} is not a valid result type of an $unapplyName method of an ${Magenta("extractor")}.$addendum""" + val explanation = if (unapplyName.show == "unapply") + hl""" + |To be used as an extractor, an unapply method has to return a type that either: + | - has members ${Magenta("isEmpty: Boolean")} and ${Magenta("get: S")} (usually an ${Green("Option[S]")}) + | - is a ${Green("Boolean")} + | - is a ${Green("Product")} (like a ${Magenta("Tuple2[T1, T2]")}) + | + |class A(val i: Int) + | + |object B { + | def unapply(a: A): ${Green("Option[Int]")} = Some(a.i) + |} + | + |object C { + | def unapply(a: A): ${Green("Boolean")} = a.i == 2 + |} + | + |object D { + | def unapply(a: A): ${Green("(Int, Int)")} = (a.i, a.i) + |} + | + |object Test { + | def test(a: A) = a match { + | ${Magenta("case B(1)")} => 1 + | ${Magenta("case a @ C()")} => 2 + | ${Magenta("case D(3, 3)")} => 3 + | } + |} + """.stripMargin + else + hl""" + |To be used as an extractor, an unapplySeq method has to return a type which has members + |${Magenta("isEmpty: Boolean")} and ${Magenta("get: S")} where ${Magenta("S <: Seq[V]")} (usually an ${Green("Option[Seq[V]]")}): + | + |object CharList { + | def unapplySeq(s: String): ${Green("Option[Seq[Char]")} = Some(s.toList) + | + | "example" match { + | ${Magenta("case CharList(c1, c2, c3, c4, _, _, _)")} => + | println(s"$$c1,$$c2,$$c3,$$c4") + | case _ => + | println("Expected *exactly* 7 characters!") + | } + |} + """.stripMargin + } + case class StaticFieldsOnlyAllowedInObjects(member: Symbol)(implicit ctx: Context) extends Message(StaticFieldsOnlyAllowedInObjectsID) { val msg: String = hl"${"@static"} $member in ${member.owner} must be defined inside an ${"object"}." val kind: String = "Syntax" diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index e9ffaeddb404..96b5edd0bfdb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -28,7 +28,7 @@ import TypeApplications._ import reporting.diagnostic.Message import reporting.trace import Constants.{Constant, IntTag, LongTag} -import dotty.tools.dotc.reporting.diagnostic.messages.{NotAnExtractor, UnapplyInvalidNumberOfArguments} +import dotty.tools.dotc.reporting.diagnostic.messages.{UnapplyInvalidReturnType, NotAnExtractor, UnapplyInvalidNumberOfArguments} import Denotations.SingleDenotation import annotation.constructorOnly @@ -99,11 +99,7 @@ object Applications { def getTp = extractorMemberType(unapplyResult, nme.get, pos) def fail = { - val addendum = - if (ctx.scala2Mode && unapplyName == nme.unapplySeq) - "\nYou might want to try to rewrite the extractor to use `unapply` instead." - else "" - ctx.error(em"$unapplyResult is not a valid result type of an $unapplyName method of an extractor$addendum", pos) + ctx.error(UnapplyInvalidReturnType(unapplyResult, unapplyName), pos) Nil } diff --git a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala index fdec58fa1933..cf6460f207d1 100644 --- a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala +++ b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala @@ -1380,6 +1380,46 @@ class ErrorMessagesTests extends ErrorMessagesTest { assertEquals("(class Int, class String)", argTypes.map(_.typeSymbol).mkString("(", ", ", ")")) } + @Test def unapplyInvalidReturnType = + checkMessagesAfter("frontend") { + """ + |class A(val i: Int) + | + |object A { + | def unapply(a: A): Int = a.i + | def test(a: A) = a match { + | case A() => 1 + | } + |} + """.stripMargin + }.expect { (ictx, messages) => + implicit val ctx: Context = ictx + assertMessageCount(1, messages) + val UnapplyInvalidReturnType(unapplyResult, unapplyName) :: Nil = messages + assertEquals("Int", unapplyResult.show) + assertEquals("unapply", unapplyName.show) + } + + @Test def unapplySeqInvalidReturnType = + checkMessagesAfter("frontend") { + """ + |class A(val i: Int) + | + |object A { + | def unapplySeq(a: A): Int = a.i + | def test(a: A) = a match { + | case A() => 1 + | } + |} + """.stripMargin + }.expect { (ictx, messages) => + implicit val ctx: Context = ictx + assertMessageCount(1, messages) + val UnapplyInvalidReturnType(unapplyResult, unapplyName) :: Nil = messages + assertEquals("Int", unapplyResult.show) + assertEquals("unapplySeq", unapplyName.show) + } + @Test def staticOnlyAllowedInsideObjects = checkMessagesAfter(CheckStatic.name) { """