Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ println("age")
println("sayHello")
```

Without having an instance of the type for nested case classes:
```scala mdoc:nest
case class Pet(age: Int)
case class Person(name: String, pet: Pet)

println(qualifiedNameOf[Person](_.pet.age))
```
```scala mdoc:nest
// 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])
Expand Down
14 changes: 14 additions & 0 deletions src/main/scala-2/com/github/dwickern/macros/NameOf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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](_.pet.age) => "pet.age"
* }}}
*/
def qualifiedNameOf[T](expr: T => Any): String = macro NameOfImpl.qualifiedNameOf

/**
* Obtain a type's unqualified name as a constant string.
*
Expand Down
20 changes: 20 additions & 0 deletions src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ object NameOfImpl {
c.Expr[String](q"$name")
}

def qualifiedNameOf(c: whitebox.Context)(expr: c.Expr[Any]): c.Expr[String] = {
import c.universe._

def extract(tree: c.Tree): List[c.Name] = tree match {
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)
case TypeApply(func, _) => extract(func)
case _ => c.abort(c.enclosingPosition, s"Unsupported expression: ${expr.tree}}")
}

val name = extract(expr.tree)
// drop sth like x$1
.drop(1)
.mkString(".")
c.Expr[String](q"$name")
}

def nameOfType[T](c: whitebox.Context)(implicit tag: c.WeakTypeTag[T]): c.Expr[String] = {
import c.universe._
val name = showRaw(tag.tpe.typeSymbol.name)
Expand Down
14 changes: 14 additions & 0 deletions src/main/scala-3/com/github/dwickern/macros/NameOf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
20 changes: 19 additions & 1 deletion src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "<special-ops>.throw" => extract(term)
case TypeApply(term, _) => extract(term)
Expand All @@ -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 != "<special-ops>.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 {
Expand Down
54 changes: 54 additions & 0 deletions src/test/scala/com/github/dwickern/macros/NameOfTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand All @@ -92,13 +97,15 @@ 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") {
object SomeObject {
lazy val member = ???
}
nameOf(SomeObject.member) should equal ("member")
qualifiedNameOf[SomeObject.type](_.member) should equal ("member")
}

test("class") {
Expand All @@ -110,7 +117,52 @@ 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") {
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")
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") {
Expand Down Expand Up @@ -189,5 +241,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) """)
}
}
12 changes: 12 additions & 0 deletions src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading