From b653df0520c44c714f98223cf227f10938f240e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Pitu=C5=82a?= Date: Tue, 24 Mar 2026 00:09:01 +0100 Subject: [PATCH 1/3] Handle named args in pattern matching --- .../scala/tastyquery/reader/tasties/TreeUnpickler.scala | 5 +++++ .../shared/src/test/scala/tastyquery/ReadTreeSuite.scala | 8 ++++++++ .../main/scala/simple_trees/NamedExtractorPattern.scala | 8 ++++++++ 3 files changed, 21 insertions(+) create mode 100644 test-sources/src/main/scala/simple_trees/NamedExtractorPattern.scala diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala index 3fcca481..0dc9f810 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala @@ -897,6 +897,11 @@ private[tasties] class TreeUnpickler private ( reader.readByte() val shared = forkAt(reader.readAddr()).readPattern if spn.isUnknown then shared else shared.withPos(spn) + case NAMEDARG => + // Named extractor patterns (e.g., `case Foo(field = value)`) wrap sub-patterns in NAMEDARG; discard the name + reader.readByte() + readUnsignedName() + readPattern case _ => val expr = readTerm ExprPattern(expr)(expr.pos) diff --git a/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala index fd5105c7..5aee5469 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala @@ -596,6 +596,14 @@ class ReadTreeSuite extends RestrictedUnpicklingSuite { assert(containsSubtree(guardWithAlternatives)(clue(tree))) } + testUnpickle("named-extractor-pattern", "simple_trees.NamedExtractorPattern") { tree => + // case Wrapper(value = Some(v)) => v + val innerUnapplyCheck: StructureCheck = { + case Unapply(_, _, List(Bind(SimpleName("v"), WildcardPattern(_), _))) => + } + assert(containsSubtree(innerUnapplyCheck)(clue(tree))) + } + testUnpickle("assign", "simple_trees.Assign") { tree => val assignBlockMatch: StructureCheck = { case Block( diff --git a/test-sources/src/main/scala/simple_trees/NamedExtractorPattern.scala b/test-sources/src/main/scala/simple_trees/NamedExtractorPattern.scala new file mode 100644 index 00000000..078d3f92 --- /dev/null +++ b/test-sources/src/main/scala/simple_trees/NamedExtractorPattern.scala @@ -0,0 +1,8 @@ +package simple_trees + +case class Wrapper(value: Option[String]) + +class NamedExtractorPattern { + def extract(items: List[Wrapper]): List[String] = + items.collect { case Wrapper(value = Some(v)) => v } +} From f707469402336b203cfb51150688af4c5c8ef36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Pitu=C5=82a?= Date: Mon, 20 Jul 2026 22:37:15 +0200 Subject: [PATCH 2/3] Preserve the name in the tree --- .../shared/src/main/scala/tastyquery/Printers.scala | 5 +++++ .../shared/src/main/scala/tastyquery/Traversers.scala | 2 ++ tasty-query/shared/src/main/scala/tastyquery/Trees.scala | 6 ++++++ .../scala/tastyquery/reader/tasties/TreeUnpickler.scala | 8 +++++--- .../shared/src/test/scala/tastyquery/ReadTreeSuite.scala | 9 ++++++--- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Printers.scala b/tasty-query/shared/src/main/scala/tastyquery/Printers.scala index 82c412ed..2d8f7c36 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Printers.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Printers.scala @@ -607,6 +607,11 @@ private[tastyquery] object Printers: case ExprPattern(expr) => print(expr) + case NamedPattern(name, body) => + print(name) + print(" = ") + print(body) + case QuotePattern(bindings, body, quotes, patternType) => print("'<") print(quotes) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala b/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala index 76842b8c..447bf9a6 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala @@ -145,6 +145,8 @@ object Traversers: traverse(patterns) case ExprPattern(expr) => traverse(expr) + case NamedPattern(name, body) => + traverse(body) case WildcardPattern(tpe) => () case QuotePattern(bindings, body, quotes, patternType) => diff --git a/tasty-query/shared/src/main/scala/tastyquery/Trees.scala b/tasty-query/shared/src/main/scala/tastyquery/Trees.scala index 1d5fca37..b1ee4925 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Trees.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Trees.scala @@ -638,6 +638,12 @@ object Trees { override final def withPos(pos: SourcePosition): ExprPattern = ExprPattern(expr)(pos) end ExprPattern + /** A named argument pattern `name = body` in an [[Unapply]], as in `case Foo(field = pat)`. */ + final case class NamedPattern(name: UnsignedTermName, body: PatternTree)(pos: SourcePosition) + extends PatternTree(pos): + override final def withPos(pos: SourcePosition): NamedPattern = NamedPattern(name, body)(pos) + end NamedPattern + /** A tree representing a quote pattern `'{ type binding1; ...; body }` or `'[ type binding1; ...; body ]`. * * The `bindings` contain the list of quote pattern type variable definitions (`TypeTreeBind`s) diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala index 0dc9f810..19eae613 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala @@ -898,10 +898,12 @@ private[tasties] class TreeUnpickler private ( val shared = forkAt(reader.readAddr()).readPattern if spn.isUnknown then shared else shared.withPos(spn) case NAMEDARG => - // Named extractor patterns (e.g., `case Foo(field = value)`) wrap sub-patterns in NAMEDARG; discard the name + // Named extractor patterns (e.g., `case Foo(field = value)`) wrap sub-patterns in NAMEDARG. + val spn = span reader.readByte() - readUnsignedName() - readPattern + val name = readUnsignedName() + val body = readPattern + NamedPattern(name, body)(spn) case _ => val expr = readTerm ExprPattern(expr)(expr.pos) diff --git a/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala index 5aee5469..79b59b90 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala @@ -598,10 +598,13 @@ class ReadTreeSuite extends RestrictedUnpicklingSuite { testUnpickle("named-extractor-pattern", "simple_trees.NamedExtractorPattern") { tree => // case Wrapper(value = Some(v)) => v - val innerUnapplyCheck: StructureCheck = { - case Unapply(_, _, List(Bind(SimpleName("v"), WildcardPattern(_), _))) => + val namedPatternCheck: StructureCheck = { + case NamedPattern( + SimpleName("value"), + TypeTest(Unapply(_, _, List(Bind(SimpleName("v"), WildcardPattern(_), _))), _) + ) => } - assert(containsSubtree(innerUnapplyCheck)(clue(tree))) + assert(containsSubtree(namedPatternCheck)(clue(tree))) } testUnpickle("assign", "simple_trees.Assign") { tree => From 6237bd1a474e52f29101ec620f76af6169f508fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Pitu=C5=82a?= Date: Mon, 20 Jul 2026 23:11:36 +0200 Subject: [PATCH 3/3] add mima exception --- build.sbt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.sbt b/build.sbt index 9e4bc030..f72e3938 100644 --- a/build.sbt +++ b/build.sbt @@ -126,6 +126,10 @@ lazy val tastyQuery = mimaBinaryIssueFilters ++= { import com.typesafe.tools.mima.core.* Seq( + // New pattern tree node for named extractor patterns `case Foo(field = pat)` + ProblemFilters.exclude[MissingClassProblem]("tastyquery.Trees$NamedPattern"), + ProblemFilters.exclude[MissingClassProblem]("tastyquery.Trees$NamedPattern$"), + ProblemFilters.exclude[MissingFieldProblem]("tastyquery.Trees.NamedPattern"), ) },