Skip to content

Commit f14ed85

Browse files
authored
Merge pull request #30 from dwickern/qualified-name-of
Add qualifiedNameOf
2 parents 29e14f9 + a92fbcb commit f14ed85

File tree

7 files changed

+146
-1
lines changed

7 files changed

+146
-1
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ println("age")
7272
println("sayHello")
7373
```
7474

75+
Without having an instance of the type for nested case classes:
76+
```scala mdoc:nest
77+
case class Pet(age: Int)
78+
case class Person(name: String, pet: Pet)
79+
80+
println(qualifiedNameOf[Person](_.pet.age))
81+
```
82+
```scala mdoc:nest
83+
// compiles to:
84+
85+
println("pet.age")
86+
```
87+
7588
You can also use `nameOfType` to get the unqualified name of a type:
7689
```scala mdoc:nest
7790
println(nameOfType[java.lang.String])

src/main/scala-2/com/github/dwickern/macros/NameOf.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ trait NameOf {
2727
*/
2828
def nameOf[T](expr: T => Any): String = macro NameOfImpl.nameOf
2929

30+
/**
31+
* Obtain a fully qualified identifier name as a constant string.
32+
*
33+
* This overload can be used to access an instance method without having an instance of the type.
34+
*
35+
* Example usage:
36+
* {{{
37+
* class Pet(val age: Int)
38+
* class Person(val name: String, val pet: Pet)
39+
* nameOf[Person](_.pet.age) => "pet.age"
40+
* }}}
41+
*/
42+
def qualifiedNameOf[T](expr: T => Any): String = macro NameOfImpl.qualifiedNameOf
43+
3044
/**
3145
* Obtain a type's unqualified name as a constant string.
3246
*

src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ object NameOfImpl {
4747
c.Expr[String](q"$name")
4848
}
4949

50+
def qualifiedNameOf(c: whitebox.Context)(expr: c.Expr[Any]): c.Expr[String] = {
51+
import c.universe._
52+
53+
def extract(tree: c.Tree): List[c.Name] = tree match {
54+
case Ident(n) => List(n.decodedName)
55+
case Select(tree, n) => extract(tree) :+ n.decodedName
56+
case Function(_, body) => extract(body)
57+
case Block(_, expr) => extract(expr)
58+
case Apply(func, _) => extract(func)
59+
case TypeApply(func, _) => extract(func)
60+
case _ => c.abort(c.enclosingPosition, s"Unsupported expression: ${expr.tree}}")
61+
}
62+
63+
val name = extract(expr.tree)
64+
// drop sth like x$1
65+
.drop(1)
66+
.mkString(".")
67+
c.Expr[String](q"$name")
68+
}
69+
5070
def nameOfType[T](c: whitebox.Context)(implicit tag: c.WeakTypeTag[T]): c.Expr[String] = {
5171
import c.universe._
5272
val name = showRaw(tag.tpe.typeSymbol.name)

src/main/scala-3/com/github/dwickern/macros/NameOf.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ trait NameOf {
2727
*/
2828
transparent inline def nameOf[T](inline expr: T => Any): String = ${NameOfImpl.nameOf('expr)}
2929

30+
/**
31+
* Obtain a fully qualified identifier name as a constant string.
32+
*
33+
* This overload can be used to access an instance method without having an instance of the type.
34+
*
35+
* Example usage:
36+
* {{{
37+
* class Pet(val age: Int)
38+
* class Person(val name: String, val pet: Pet)
39+
* nameOf[Person](_.pet.age) => "pet.age"
40+
* }}}
41+
*/
42+
transparent inline def qualifiedNameOf[T](inline expr: T => Any): String = ${NameOfImpl.qualifiedNameOf('expr)}
43+
3044
/**
3145
* Obtain a type's unqualified name as a constant string.
3246
*

src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ object NameOfImpl {
99
@tailrec def extract(tree: Tree): String = tree match {
1010
case Ident(name) => name
1111
case Select(_, name) => name
12-
case Block(List(stmt), term) => extract(stmt)
1312
case DefDef("$anonfun", _, _, Some(term)) => extract(term)
13+
case Block(List(stmt), _) => extract(stmt)
1414
case Block(_, term) => extract(term)
1515
case Apply(term, _) if term.symbol.fullName != "<special-ops>.throw" => extract(term)
1616
case TypeApply(term, _) => extract(term)
@@ -22,6 +22,24 @@ object NameOfImpl {
2222
Expr(name)
2323
}
2424

25+
def qualifiedNameOf(expr: Expr[Any])(using Quotes): Expr[String] = {
26+
import quotes.reflect.*
27+
def extract(tree: Tree): List[String] = tree match {
28+
case Ident(name) => List(name)
29+
case Select(tree, name) => extract(tree) :+ name
30+
case DefDef("$anonfun", _, _, Some(term)) => extract(term)
31+
case Block(List(stmt), _) => extract(stmt)
32+
case Block(_, term) => extract(term)
33+
case Apply(term, _) if term.symbol.fullName != "<special-ops>.throw" => extract(term)
34+
case TypeApply(term, _) => extract(term)
35+
case Inlined(_, _, term) => extract(term)
36+
case Typed(term, _) => extract(term)
37+
case _ => throw new MatchError(s"Unsupported expression: ${expr.show}")
38+
}
39+
val name = extract(expr.asTerm).drop(1).mkString(".")
40+
Expr(name)
41+
}
42+
2543
def nameOfType[T](using Quotes, Type[T]): Expr[String] = {
2644
import quotes.reflect.*
2745
val name = TypeTree.of[T].tpe.dealias match {

src/test/scala/com/github/dwickern/macros/NameOfTest.scala

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ class NameOfTest extends AnyFunSuite with Matchers {
7474
nameOf(generic(???)) should equal ("generic")
7575
}
7676

77+
test("identity function") {
78+
nameOf[String](x => x) should equal ("x")
79+
qualifiedNameOf[String](x => x) should equal ("")
80+
}
81+
7782
test("instance member") {
7883
class SomeClass(val foobar: String)
7984
val myClass = new SomeClass("")
@@ -92,13 +97,15 @@ class NameOfTest extends AnyFunSuite with Matchers {
9297
nameOf[SomeClass](_.foobar) should equal ("foobar")
9398
nameOf { (c: SomeClass) => c.foobar } should equal ("foobar")
9499
nameOf((_: SomeClass).foobar) should equal ("foobar")
100+
qualifiedNameOf[SomeClass](_.foobar) should equal ("foobar")
95101
}
96102

97103
test("object member") {
98104
object SomeObject {
99105
lazy val member = ???
100106
}
101107
nameOf(SomeObject.member) should equal ("member")
108+
qualifiedNameOf[SomeObject.type](_.member) should equal ("member")
102109
}
103110

104111
test("class") {
@@ -110,7 +117,52 @@ class NameOfTest extends AnyFunSuite with Matchers {
110117
nameOf(CaseClass) should equal ("CaseClass")
111118
nameOfType[CaseClass] should equal ("CaseClass")
112119
qualifiedNameOfType[CaseClass] should equal ("com.github.dwickern.macros.CaseClass")
120+
}
121+
122+
test("nested case class member") {
123+
case class Nested3CaseClass(member: String)
124+
case class Nested2CaseClass(nested3CaseClass: Nested3CaseClass)
125+
case class Nested1CaseClass(nested2CaseClass: Nested2CaseClass)
126+
case class CaseClass(nested1CaseClass: Nested1CaseClass)
127+
128+
qualifiedNameOf[CaseClass](_.nested1CaseClass.nested2CaseClass.nested3CaseClass.member) should equal("nested1CaseClass.nested2CaseClass.nested3CaseClass.member")
129+
qualifiedNameOf((cc: CaseClass) => cc.nested1CaseClass.nested2CaseClass) should equal("nested1CaseClass.nested2CaseClass")
130+
}
131+
132+
test("nested Java method calls") {
133+
qualifiedNameOf[String](_.length.toLong) should equal("length.toLong")
134+
qualifiedNameOf[String](_.length().toString()) should equal("length.toString")
135+
qualifiedNameOf[String] { str => str.length().toString } should equal("length.toString")
136+
}
137+
138+
test("nested symbolic members") {
139+
class C1(val `multi word name`: C2)
140+
class C2(val 你好: C3)
141+
class C3(val ??? : String)
142+
143+
qualifiedNameOf[C1](_.`multi word name`.你好.???) should equal ("multi word name.你好.???")
144+
}
145+
146+
test("nested generic members") {
147+
trait T1 {
148+
def foo[T]: T2 = ???
149+
}
150+
trait T2 {
151+
def bar[T]: Int = ???
152+
}
153+
154+
qualifiedNameOf[T1](_.foo.bar) should equal ("foo.bar")
155+
}
156+
157+
test("nested function call") {
158+
class C1(val c2: C2)
159+
class C2(val c3: C3.type)
160+
object C3 {
161+
def func(x: Int) = ???
162+
}
113163

164+
qualifiedNameOf[C1](_.c2.c3.func _) should equal ("c2.c3.func")
165+
qualifiedNameOf[C1](_.c2.c3.func(???)) should equal ("c2.c3.func")
114166
}
115167

116168
test("object") {
@@ -189,5 +241,7 @@ class NameOfTest extends AnyFunSuite with Matchers {
189241
illTyped(""" nameOf(true) """, "Unsupported constant expression: true")
190242
illTyped(""" nameOf(null) """, "Unsupported constant expression: null")
191243
illTyped(""" nameOf() """, "Unsupported constant expression: \\(\\)")
244+
illTyped(""" qualifiedNameOf[String](_ => ()) """)
245+
illTyped(""" qualifiedNameOf[String](_ => 3) """)
192246
}
193247
}

src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ class AnnotationTest extends AnyFunSuite with Matchers {
3535
annotation.name should === ("classMember")
3636
}
3737

38+
test("qualifiedNameOf") {
39+
class C(val foo: Foo)
40+
class Foo(val bar: Bar)
41+
class Bar(val baz: String)
42+
43+
@Resource(name = qualifiedNameOf[C](_.foo.bar.baz))
44+
class AnnotatedClass
45+
46+
val annotation = classOf[AnnotatedClass].getDeclaredAnnotation(classOf[Resource])
47+
annotation.name should === ("foo.bar.baz")
48+
}
49+
3850
test("nameOfType") {
3951
@Resource(name = nameOfType[AnnotatedClass])
4052
class AnnotatedClass

0 commit comments

Comments
 (0)