From a85ee648bd2803edece9692b2d55b2db70d966c7 Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Mon, 3 Mar 2025 15:06:17 +0000 Subject: [PATCH] Refuse trailing type parameters in extractors Co-authored-by: Dimi Racordon [Cherry-picked 94b0b13a4f6b0d68de07804c6b17b36bbefea6e7] --- .../dotty/tools/dotc/reporting/messages.scala | 6 ++- .../dotty/tools/dotc/typer/Applications.scala | 23 +++++++++++- tests/neg/22550.check | 37 +++++++++++++++++++ tests/neg/22550.scala | 8 ++++ tests/neg/22550b.check | 37 +++++++++++++++++++ tests/neg/22550b.scala | 16 ++++++++ tests/neg/bad-unapplies.check | 4 +- tests/neg/i18684.check | 6 ++- 8 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 tests/neg/22550.check create mode 100644 tests/neg/22550.scala create mode 100644 tests/neg/22550b.check create mode 100644 tests/neg/22550b.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 6c1f8ed939d6..333e6ad83c31 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2381,13 +2381,15 @@ class ClassCannotExtendEnum(cls: Symbol, parent: Symbol)(using Context) extends } class NotAnExtractor(tree: untpd.Tree)(using Context) extends PatternMatchMsg(NotAnExtractorID) { - def msg(using Context) = i"$tree cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method" + def msg(using Context) = i"$tree cannot be used as an extractor in a pattern because it lacks an ${hl("unapply")} or ${hl("unapplySeq")} method with the appropriate signature" def explain(using Context) = - i"""|An ${hl("unapply")} method should be defined in an ${hl("object")} as follow: + i"""|An ${hl("unapply")} method should be in an ${hl("object")}, take a single explicit term parameter, and: | - If it is just a test, return a ${hl("Boolean")}. For example ${hl("case even()")} | - If it returns a single sub-value of type T, return an ${hl("Option[T]")} | - If it returns several sub-values T1,...,Tn, group them in an optional tuple ${hl("Option[(T1,...,Tn)]")} | + |Additionaly, ${hl("unapply")} or ${hl("unapplySeq")} methods cannot take type parameters after their explicit term parameter. + | |Sometimes, the number of sub-values isn't fixed and we would like to return a sequence. |For this reason, you can also define patterns through ${hl("unapplySeq")} which returns ${hl("Option[Seq[T]]")}. |This mechanism is used for instance in pattern ${hl("case List(x1, ..., xn)")}""" diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 87e9a39c7c57..3d65843bc369 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -35,6 +35,7 @@ import annotation.threadUnsafe import scala.util.control.NonFatal import dotty.tools.dotc.inlines.Inlines +import scala.annotation.tailrec object Applications { import tpd.* @@ -1428,6 +1429,20 @@ trait Applications extends Compatibility { def trySelectUnapply(qual: untpd.Tree)(fallBack: (Tree, TyperState) => Tree): Tree = { // try first for non-overloaded, then for overloaded occurrences def tryWithName(name: TermName)(fallBack: (Tree, TyperState) => Tree)(using Context): Tree = + /** Returns `true` if there are type parameters after the last explicit + * (non-implicit) term parameters list. + */ + @tailrec + def hasTrailingTypeParams(paramss: List[List[Symbol]], acc: Boolean = false): Boolean = + paramss match + case Nil => acc + case params :: rest => + val newAcc = + params match + case param :: _ if param.isType => true + case param :: _ if param.isTerm && !param.isOneOf(GivenOrImplicit) => false + case _ => acc + hasTrailingTypeParams(paramss.tail, newAcc) def tryWithProto(qual: untpd.Tree, targs: List[Tree], pt: Type)(using Context) = val proto = UnapplyFunProto(pt, this) @@ -1435,7 +1450,13 @@ trait Applications extends Compatibility { val result = if targs.isEmpty then typedExpr(unapp, proto) else typedExpr(unapp, PolyProto(targs, proto)).appliedToTypeTrees(targs) - if !result.symbol.exists + if result.symbol.exists && hasTrailingTypeParams(result.symbol.paramSymss) then + // We don't accept `unapply` or `unapplySeq` methods with type + // parameters after the last explicit term parameter because we + // can't encode them: `UnApply` nodes cannot take type paremeters. + // See #22550 and associated test cases. + notAnExtractor(result) + else if !result.symbol.exists || result.symbol.name == name || ctx.reporter.hasErrors then result diff --git a/tests/neg/22550.check b/tests/neg/22550.check new file mode 100644 index 000000000000..6a3eb2c2ac83 --- /dev/null +++ b/tests/neg/22550.check @@ -0,0 +1,37 @@ +-- [E127] Pattern Match Error: tests/neg/22550.scala:8:9 --------------------------------------------------------------- +8 | case Matches(x) => println(x) // error // error + | ^^^^^^^ + |Matches cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature + |--------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | An unapply method should be in an object, take a single explicit term parameter, and: + | - If it is just a test, return a Boolean. For example case even() + | - If it returns a single sub-value of type T, return an Option[T] + | - If it returns several sub-values T1,...,Tn, group them in an optional tuple Option[(T1,...,Tn)] + | + | Additionaly, unapply or unapplySeq methods cannot take type parameters after their explicit term parameter. + | + | Sometimes, the number of sub-values isn't fixed and we would like to return a sequence. + | For this reason, you can also define patterns through unapplySeq which returns Option[Seq[T]]. + | This mechanism is used for instance in pattern case List(x1, ..., xn) + --------------------------------------------------------------------------------------------------------------------- +-- [E006] Not Found Error: tests/neg/22550.scala:8:31 ------------------------------------------------------------------ +8 | case Matches(x) => println(x) // error // error + | ^ + | Not found: x + |--------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Each identifier in Scala needs a matching declaration. There are two kinds of + | identifiers: type identifiers and value identifiers. Value identifiers are introduced + | by `val`, `def`, or `object` declarations. Type identifiers are introduced by `type`, + | `class`, `enum`, or `trait` declarations. + | + | Identifiers refer to matching declarations in their environment, or they can be + | imported from elsewhere. + | + | Possible reasons why no matching declaration was found: + | - The declaration or the use is mis-spelt. + | - An import is missing. + --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg/22550.scala b/tests/neg/22550.scala new file mode 100644 index 000000000000..d7444139e656 --- /dev/null +++ b/tests/neg/22550.scala @@ -0,0 +1,8 @@ +//> using options -explain + +object Matches: + def unapply(y: Any)[T]: Option[Any] = None + +def main = + 42 match + case Matches(x) => println(x) // error // error diff --git a/tests/neg/22550b.check b/tests/neg/22550b.check new file mode 100644 index 000000000000..8e2145260946 --- /dev/null +++ b/tests/neg/22550b.check @@ -0,0 +1,37 @@ +-- [E127] Pattern Match Error: tests/neg/22550b.scala:16:9 ------------------------------------------------------------- +16 | case Matches[Unit](x) => println(x) // error // error + | ^^^^^^^^^^^^ + |Matches[Unit] cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | An unapply method should be in an object, take a single explicit term parameter, and: + | - If it is just a test, return a Boolean. For example case even() + | - If it returns a single sub-value of type T, return an Option[T] + | - If it returns several sub-values T1,...,Tn, group them in an optional tuple Option[(T1,...,Tn)] + | + | Additionaly, unapply or unapplySeq methods cannot take type parameters after their explicit term parameter. + | + | Sometimes, the number of sub-values isn't fixed and we would like to return a sequence. + | For this reason, you can also define patterns through unapplySeq which returns Option[Seq[T]]. + | This mechanism is used for instance in pattern case List(x1, ..., xn) + -------------------------------------------------------------------------------------------------------------------- +-- [E006] Not Found Error: tests/neg/22550b.scala:16:37 ---------------------------------------------------------------- +16 | case Matches[Unit](x) => println(x) // error // error + | ^ + | Not found: x + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Each identifier in Scala needs a matching declaration. There are two kinds of + | identifiers: type identifiers and value identifiers. Value identifiers are introduced + | by `val`, `def`, or `object` declarations. Type identifiers are introduced by `type`, + | `class`, `enum`, or `trait` declarations. + | + | Identifiers refer to matching declarations in their environment, or they can be + | imported from elsewhere. + | + | Possible reasons why no matching declaration was found: + | - The declaration or the use is mis-spelt. + | - An import is missing. + -------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg/22550b.scala b/tests/neg/22550b.scala new file mode 100644 index 000000000000..2f3dc1c2445d --- /dev/null +++ b/tests/neg/22550b.scala @@ -0,0 +1,16 @@ +//> using options -explain + +case class Data[A]() + +trait TCl[A, B] + +object Matches: + def unapply[A](adt: Data[?])[B](using + ft: TCl[A, B] + ): Option[Data[A]] = None + +given TCl[Unit, String] = new TCl {} + +def main = + Data() match + case Matches[Unit](x) => println(x) // error // error diff --git a/tests/neg/bad-unapplies.check b/tests/neg/bad-unapplies.check index 51e71d8e8949..54844e1ff8a0 100644 --- a/tests/neg/bad-unapplies.check +++ b/tests/neg/bad-unapplies.check @@ -10,13 +10,13 @@ -- [E127] Pattern Match Error: tests/neg/bad-unapplies.scala:23:9 ------------------------------------------------------ 23 | case B("2") => // error (cannot be used as an extractor) | ^ - | B cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method + |B cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature | | longer explanation available when compiling with `-explain` -- [E127] Pattern Match Error: tests/neg/bad-unapplies.scala:24:9 ------------------------------------------------------ 24 | case D("2") => // error (cannot be used as an extractor) | ^ - | D cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method + |D cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature | | longer explanation available when compiling with `-explain` -- [E050] Type Error: tests/neg/bad-unapplies.scala:25:9 --------------------------------------------------------------- diff --git a/tests/neg/i18684.check b/tests/neg/i18684.check index 5dc4a2fdd736..908e92b697a5 100644 --- a/tests/neg/i18684.check +++ b/tests/neg/i18684.check @@ -67,15 +67,17 @@ -- [E127] Pattern Match Error: tests/neg/i18684.scala:12:6 ------------------------------------------------------------- 12 | val inner(x) = 3 // error | ^^^^^ - | Test.inner cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method + |Test.inner cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature |-------------------------------------------------------------------------------------------------------------------- | Explanation (enabled by `-explain`) |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | An unapply method should be defined in an object as follow: + | An unapply method should be in an object, take a single explicit term parameter, and: | - If it is just a test, return a Boolean. For example case even() | - If it returns a single sub-value of type T, return an Option[T] | - If it returns several sub-values T1,...,Tn, group them in an optional tuple Option[(T1,...,Tn)] | + | Additionaly, unapply or unapplySeq methods cannot take type parameters after their explicit term parameter. + | | Sometimes, the number of sub-values isn't fixed and we would like to return a sequence. | For this reason, you can also define patterns through unapplySeq which returns Option[Seq[T]]. | This mechanism is used for instance in pattern case List(x1, ..., xn)