From f06f39d93f2cb8b9665bc5f741c59a2a77786368 Mon Sep 17 00:00:00 2001 From: Andreas Gabor Date: Thu, 3 Jun 2021 17:03:09 +0200 Subject: [PATCH 1/9] added qualifiedNameOf --- README.md | 14 ++++++++ .../com/github/dwickern/macros/NameOf.scala | 14 ++++++++ .../github/dwickern/macros/NameOfImpl.scala | 33 ++++++++++++++++++- .../github/dwickern/macros/NameOfTest.scala | 9 +++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7959254..e193f38 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,20 @@ println("age") println("sayHello") ``` +Without having an instance of the type for nested case classes: +```scala + import com.github.dwickern.macros.NameOf._ + + case class Pet(age: Int) + case class Person(name: String, pet : Pet) + + println(qualifiedNameOf[Person](_.pet.age)) + + // compiles to: + + println("pet.age") +``` + You can also use `nameOfType` to get the unqualified name of a type: ```scala mdoc:nest println(nameOfType[java.lang.String]) diff --git a/src/main/scala/com/github/dwickern/macros/NameOf.scala b/src/main/scala/com/github/dwickern/macros/NameOf.scala index 7112591..7547969 100644 --- a/src/main/scala/com/github/dwickern/macros/NameOf.scala +++ b/src/main/scala/com/github/dwickern/macros/NameOf.scala @@ -27,6 +27,20 @@ trait NameOf { */ def nameOf[T](expr: T => Any): String = macro NameOfImpl.nameOf + /** + * Obtain a fully qualified identifier name as a constant string. + * + * This overload can be used to access an instance method without having an instance of the type. + * + * Example usage: + * {{{ + * class Pet(val age: Int) + * class Person(val name: String, val pet : Pet) + * nameOf[Person](_.peta.age) => "pet.age" + * }}} + */ + def qualifiedNameOf[T](expr: T => Any): String = macro NameOfImpl.qualifiedNameOf + /** * Obtain a type's unqualified name as a constant string. * diff --git a/src/main/scala/com/github/dwickern/macros/NameOfImpl.scala b/src/main/scala/com/github/dwickern/macros/NameOfImpl.scala index 15ff8c0..6a06b52 100644 --- a/src/main/scala/com/github/dwickern/macros/NameOfImpl.scala +++ b/src/main/scala/com/github/dwickern/macros/NameOfImpl.scala @@ -1,7 +1,7 @@ package com.github.dwickern.macros import scala.language.experimental.macros -import scala.reflect.macros.whitebox +import scala.reflect.macros.{blackbox, whitebox} import scala.annotation.tailrec object NameOfImpl { @@ -47,6 +47,37 @@ object NameOfImpl { c.Expr[String](q"$name") } + def qualifiedNameOf(c: blackbox.Context)(expr: c.Expr[Any]): c.Expr[String] = { + import c.universe._ + + def extractNames(tree: c.Tree): List[c.Name] = { + tree.children.headOption match { + case Some(child) => + extractNames(child) :+ tree.symbol.name + case None => + List(tree.symbol.name) + } + } + + @tailrec def extract(tree: c.Tree): List[c.Name] = tree match { + case Ident(n) => List(n) + case Select(tree, n) => extractNames(tree) :+ n + case Function(_, body) => extract(body) + case Block(_, expr) => extract(expr) + case Apply(func, _) => extract(func) + case TypeApply(func, _) => extract(func) + case _ => c.abort(c.enclosingPosition, s"Unsupported expression: $expr") + } + + val name = extract(expr.tree) + // drop sth like x$1 + .drop(1) + .mkString(".") + reify { + c.Expr[String] { Literal(Constant(name)) }.splice + } + } + def nameOfType[T](c: whitebox.Context)(implicit tag: c.WeakTypeTag[T]): c.Expr[String] = { import c.universe._ val name = showRaw(tag.tpe.typeSymbol.name) diff --git a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala index 29696bf..a9c4cf2 100644 --- a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala +++ b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala @@ -104,6 +104,15 @@ class NameOfTest extends AnyFunSuite with Matchers { qualifiedNameOfType[CaseClass] should equal ("com.github.dwickern.macros.NameOfTest.CaseClass") } + test("nested case class member") { + case class Nested3CaseClass(member: String) + case class Nested2CaseClass(nested3CaseClass: Nested3CaseClass) + case class Nested1CaseClass(nested2CaseClass: Nested2CaseClass) + case class CaseClass(nested1CaseClass: Nested1CaseClass) + + qualifiedNameOf[CaseClass](_.nested1CaseClass.nested2CaseClass.nested3CaseClass.member) should equal("nested1CaseClass.nested2CaseClass.nested3CaseClass.member") + } + test("object") { object SomeObject nameOf(SomeObject) should equal ("SomeObject") From 8b0c0cdaacb24033b835ddf1b19776f91284df7a Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 13:32:46 -0800 Subject: [PATCH 2/9] fix typo --- src/main/scala-2/com/github/dwickern/macros/NameOf.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala-2/com/github/dwickern/macros/NameOf.scala b/src/main/scala-2/com/github/dwickern/macros/NameOf.scala index 7547969..b6c10f4 100644 --- a/src/main/scala-2/com/github/dwickern/macros/NameOf.scala +++ b/src/main/scala-2/com/github/dwickern/macros/NameOf.scala @@ -35,8 +35,8 @@ trait NameOf { * Example usage: * {{{ * class Pet(val age: Int) - * class Person(val name: String, val pet : Pet) - * nameOf[Person](_.peta.age) => "pet.age" + * class Person(val name: String, val pet: Pet) + * nameOf[Person](_.pet.age) => "pet.age" * }}} */ def qualifiedNameOf[T](expr: T => Any): String = macro NameOfImpl.qualifiedNameOf From f57cb175c57acee52dafe70aa20a1f2fccfa4ce7 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 13:33:19 -0800 Subject: [PATCH 3/9] allow usage in annotations --- .../com/github/dwickern/macros/NameOfImpl.scala | 4 ++-- .../com/github/dwickern/macros/AnnotationTest.scala | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala index 6a06b52..d304c7f 100644 --- a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala +++ b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala @@ -1,7 +1,7 @@ package com.github.dwickern.macros import scala.language.experimental.macros -import scala.reflect.macros.{blackbox, whitebox} +import scala.reflect.macros.whitebox import scala.annotation.tailrec object NameOfImpl { @@ -47,7 +47,7 @@ object NameOfImpl { c.Expr[String](q"$name") } - def qualifiedNameOf(c: blackbox.Context)(expr: c.Expr[Any]): c.Expr[String] = { + def qualifiedNameOf(c: whitebox.Context)(expr: c.Expr[Any]): c.Expr[String] = { import c.universe._ def extractNames(tree: c.Tree): List[c.Name] = { diff --git a/src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala b/src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala index 56b8651..a8f6bfe 100644 --- a/src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala +++ b/src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala @@ -35,6 +35,18 @@ class AnnotationTest extends AnyFunSuite with Matchers { annotation.name should === ("classMember") } + test("qualifiedNameOf") { + class C(val foo: Foo) + class Foo(val bar: Bar) + class Bar(val baz: String) + + @Resource(name = qualifiedNameOf[C](_.foo.bar.baz)) + class AnnotatedClass + + val annotation = classOf[AnnotatedClass].getDeclaredAnnotation(classOf[Resource]) + annotation.name should === ("foo.bar.baz") + } + test("nameOfType") { @Resource(name = nameOfType[AnnotatedClass]) class AnnotatedClass From 2f67a7b60c373efd312e481cccf48c11f2e0a049 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 15:51:01 -0800 Subject: [PATCH 4/9] simplify implementation --- .../github/dwickern/macros/NameOfImpl.scala | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala index d304c7f..b05983e 100644 --- a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala +++ b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala @@ -50,32 +50,21 @@ object NameOfImpl { def qualifiedNameOf(c: whitebox.Context)(expr: c.Expr[Any]): c.Expr[String] = { import c.universe._ - def extractNames(tree: c.Tree): List[c.Name] = { - tree.children.headOption match { - case Some(child) => - extractNames(child) :+ tree.symbol.name - case None => - List(tree.symbol.name) - } - } - - @tailrec def extract(tree: c.Tree): List[c.Name] = tree match { + def extract(tree: c.Tree): List[c.Name] = tree match { case Ident(n) => List(n) - case Select(tree, n) => extractNames(tree) :+ n + case Select(tree, n) => extract(tree) :+ n case Function(_, body) => extract(body) case Block(_, expr) => extract(expr) case Apply(func, _) => extract(func) case TypeApply(func, _) => extract(func) - case _ => c.abort(c.enclosingPosition, s"Unsupported expression: $expr") + case _ => c.abort(c.enclosingPosition, s"Unsupported expression: ${expr.tree}}") } val name = extract(expr.tree) // drop sth like x$1 .drop(1) .mkString(".") - reify { - c.Expr[String] { Literal(Constant(name)) }.splice - } + c.Expr[String](q"$name") } def nameOfType[T](c: whitebox.Context)(implicit tag: c.WeakTypeTag[T]): c.Expr[String] = { From e4943ccd9f4c6c741714bd462ec690239bd18de6 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 15:51:22 -0800 Subject: [PATCH 5/9] fix symbolic names --- src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala index b05983e..2a92567 100644 --- a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala +++ b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala @@ -51,8 +51,8 @@ object NameOfImpl { import c.universe._ def extract(tree: c.Tree): List[c.Name] = tree match { - case Ident(n) => List(n) - case Select(tree, n) => extract(tree) :+ n + case Ident(n) => List(n.decodedName) + case Select(tree, n) => extract(tree) :+ n.decodedName case Function(_, body) => extract(body) case Block(_, expr) => extract(expr) case Apply(func, _) => extract(func) From b954a3716d05794637c72b605a1f510441c2ae62 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 15:52:07 -0800 Subject: [PATCH 6/9] implement scala 3 macro --- .../com/github/dwickern/macros/NameOf.scala | 14 +++++++++++++ .../github/dwickern/macros/NameOfImpl.scala | 20 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/scala-3/com/github/dwickern/macros/NameOf.scala b/src/main/scala-3/com/github/dwickern/macros/NameOf.scala index 45a4d45..8f78aee 100644 --- a/src/main/scala-3/com/github/dwickern/macros/NameOf.scala +++ b/src/main/scala-3/com/github/dwickern/macros/NameOf.scala @@ -27,6 +27,20 @@ trait NameOf { */ transparent inline def nameOf[T](inline expr: T => Any): String = ${NameOfImpl.nameOf('expr)} + /** + * Obtain a fully qualified identifier name as a constant string. + * + * This overload can be used to access an instance method without having an instance of the type. + * + * Example usage: + * {{{ + * class Pet(val age: Int) + * class Person(val name: String, val pet: Pet) + * nameOf[Person](_.pet.age) => "pet.age" + * }}} + */ + transparent inline def qualifiedNameOf[T](inline expr: T => Any): String = ${NameOfImpl.qualifiedNameOf('expr)} + /** * Obtain a type's unqualified name as a constant string. * diff --git a/src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala b/src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala index 1ac6019..839a9c3 100644 --- a/src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala +++ b/src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala @@ -9,8 +9,8 @@ object NameOfImpl { @tailrec def extract(tree: Tree): String = tree match { case Ident(name) => name case Select(_, name) => name - case Block(List(stmt), term) => extract(stmt) case DefDef("$anonfun", _, _, Some(term)) => extract(term) + case Block(List(stmt), _) => extract(stmt) case Block(_, term) => extract(term) case Apply(term, _) if term.symbol.fullName != ".throw" => extract(term) case TypeApply(term, _) => extract(term) @@ -22,6 +22,24 @@ object NameOfImpl { Expr(name) } + def qualifiedNameOf(expr: Expr[Any])(using Quotes): Expr[String] = { + import quotes.reflect.* + def extract(tree: Tree): List[String] = tree match { + case Ident(name) => List(name) + case Select(tree, name) => extract(tree) :+ name + case DefDef("$anonfun", _, _, Some(term)) => extract(term) + case Block(List(stmt), _) => extract(stmt) + case Block(_, term) => extract(term) + case Apply(term, _) if term.symbol.fullName != ".throw" => extract(term) + case TypeApply(term, _) => extract(term) + case Inlined(_, _, term) => extract(term) + case Typed(term, _) => extract(term) + case _ => throw new MatchError(s"Unsupported expression: ${expr.show}") + } + val name = extract(expr.asTerm).drop(1).mkString(".") + Expr(name) + } + def nameOfType[T](using Quotes, Type[T]): Expr[String] = { import quotes.reflect.* val name = TypeTree.of[T].tpe.dealias match { From 5a3fb2043468aa5fed4d24e899baa947ef807538 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 15:53:03 -0800 Subject: [PATCH 7/9] add tests --- .../github/dwickern/macros/NameOfTest.scala | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala index 993a8b5..c74b7bf 100644 --- a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala +++ b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala @@ -92,6 +92,7 @@ class NameOfTest extends AnyFunSuite with Matchers { nameOf[SomeClass](_.foobar) should equal ("foobar") nameOf { (c: SomeClass) => c.foobar } should equal ("foobar") nameOf((_: SomeClass).foobar) should equal ("foobar") + qualifiedNameOf[SomeClass](_.foobar) should equal ("foobar") } test("object member") { @@ -99,6 +100,7 @@ class NameOfTest extends AnyFunSuite with Matchers { lazy val member = ??? } nameOf(SomeObject.member) should equal ("member") + qualifiedNameOf[SomeObject.type](_.member) should equal ("member") } test("class") { @@ -110,7 +112,6 @@ class NameOfTest extends AnyFunSuite with Matchers { nameOf(CaseClass) should equal ("CaseClass") nameOfType[CaseClass] should equal ("CaseClass") qualifiedNameOfType[CaseClass] should equal ("com.github.dwickern.macros.CaseClass") - } test("nested case class member") { @@ -120,6 +121,43 @@ class NameOfTest extends AnyFunSuite with Matchers { case class CaseClass(nested1CaseClass: Nested1CaseClass) qualifiedNameOf[CaseClass](_.nested1CaseClass.nested2CaseClass.nested3CaseClass.member) should equal("nested1CaseClass.nested2CaseClass.nested3CaseClass.member") + qualifiedNameOf((cc: CaseClass) => cc.nested1CaseClass.nested2CaseClass) should equal("nested1CaseClass.nested2CaseClass") + } + + test("nested Java method calls") { + qualifiedNameOf[String](_.length.toLong) should equal("length.toLong") + qualifiedNameOf[String](_.length().toString()) should equal("length.toString") + qualifiedNameOf[String] { str => str.length().toString } should equal("length.toString") + } + + test("nested symbolic members") { + class C1(val `multi word name`: C2) + class C2(val 你好: C3) + class C3(val ??? : String) + + qualifiedNameOf[C1](_.`multi word name`.你好.???) should equal ("multi word name.你好.???") + } + + test("nested generic members") { + trait T1 { + def foo[T]: T2 = ??? + } + trait T2 { + def bar[T]: Int = ??? + } + + qualifiedNameOf[T1](_.foo.bar) should equal ("foo.bar") + } + + test("nested function call") { + class C1(val c2: C2) + class C2(val c3: C3.type) + object C3 { + def func(x: Int) = ??? + } + + qualifiedNameOf[C1](_.c2.c3.func _) should equal ("c2.c3.func") + qualifiedNameOf[C1](_.c2.c3.func(???)) should equal ("c2.c3.func") } test("object") { @@ -198,5 +236,7 @@ class NameOfTest extends AnyFunSuite with Matchers { illTyped(""" nameOf(true) """, "Unsupported constant expression: true") illTyped(""" nameOf(null) """, "Unsupported constant expression: null") illTyped(""" nameOf() """, "Unsupported constant expression: \\(\\)") + illTyped(""" qualifiedNameOf[String](_ => ()) """) + illTyped(""" qualifiedNameOf[String](_ => 3) """) } } From 2fb7cbde31bb3a140f0b3cf564254075d9fc5dc2 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 8 Mar 2025 15:53:16 -0800 Subject: [PATCH 8/9] update readme --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4e8a566..e5ddc71 100644 --- a/README.md +++ b/README.md @@ -73,17 +73,16 @@ println("sayHello") ``` Without having an instance of the type for nested case classes: -```scala - import com.github.dwickern.macros.NameOf._ - - case class Pet(age: Int) - case class Person(name: String, pet : Pet) - - println(qualifiedNameOf[Person](_.pet.age)) +```scala mdoc:nest +case class Pet(age: Int) +case class Person(name: String, pet: Pet) - // compiles to: +println(qualifiedNameOf[Person](_.pet.age)) +``` +```scala mdoc:nest +// compiles to: - println("pet.age") +println("pet.age") ``` You can also use `nameOfType` to get the unqualified name of a type: From a92fbcbb1eb6ac7017ac1628356fab666b965849 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Mon, 10 Mar 2025 13:06:45 -0700 Subject: [PATCH 9/9] add test for identity function --- src/test/scala/com/github/dwickern/macros/NameOfTest.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala index c74b7bf..b6f1171 100644 --- a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala +++ b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala @@ -74,6 +74,11 @@ class NameOfTest extends AnyFunSuite with Matchers { nameOf(generic(???)) should equal ("generic") } + test("identity function") { + nameOf[String](x => x) should equal ("x") + qualifiedNameOf[String](x => x) should equal ("") + } + test("instance member") { class SomeClass(val foobar: String) val myClass = new SomeClass("")