diff --git a/README.md b/README.md index 54b83e1..e5ddc71 100644 --- a/README.md +++ b/README.md @@ -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]) 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 7112591..b6c10f4 100644 --- a/src/main/scala-2/com/github/dwickern/macros/NameOf.scala +++ b/src/main/scala-2/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](_.pet.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-2/com/github/dwickern/macros/NameOfImpl.scala b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala index 15ff8c0..2a92567 100644 --- a/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala +++ b/src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala @@ -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) 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 { diff --git a/src/test/scala/com/github/dwickern/macros/NameOfTest.scala b/src/test/scala/com/github/dwickern/macros/NameOfTest.scala index f47b786..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("") @@ -92,6 +97,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 +105,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 +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") { @@ -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) """) } } 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