├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── src ├── test │ ├── scala-2 │ │ ├── com │ │ │ └── github │ │ │ │ └── dwickern │ │ │ │ └── macros │ │ │ │ └── test │ │ │ │ └── package.scala │ │ └── thirdparty │ │ │ └── CustomDslTest.scala │ ├── scala-3 │ │ ├── com │ │ │ └── github │ │ │ │ └── dwickern │ │ │ │ └── macros │ │ │ │ ├── test │ │ │ │ └── package.scala │ │ │ │ └── Scala3FeaturesTest.scala │ │ └── thirdparty │ │ │ └── CustomDslTest.scala │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── dwickern │ │ │ └── macros │ │ │ └── JavaConstants.java │ ├── scala │ │ └── com │ │ │ └── github │ │ │ └── dwickern │ │ │ └── macros │ │ │ ├── ConstantsTest.scala │ │ │ └── NameOfTest.scala │ └── scalajvm │ │ └── com │ │ └── github │ │ └── dwickern │ │ └── macros │ │ └── AnnotationTest.scala └── main │ ├── scala-2 │ └── com │ │ └── github │ │ └── dwickern │ │ └── macros │ │ ├── NameOf.scala │ │ └── NameOfImpl.scala │ └── scala-3 │ └── com │ └── github │ └── dwickern │ └── macros │ ├── NameOf.scala │ └── NameOfImpl.scala ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── LICENSE.md └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.9.9 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .bsp/ 3 | .bloop/ 4 | .idea/ 5 | .metals/ 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /src/test/scala-2/com/github/dwickern/macros/test/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | package object test { 4 | val illTyped = shapeless.test.illTyped 5 | } 6 | -------------------------------------------------------------------------------- /src/test/scala-3/com/github/dwickern/macros/test/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | package object test { 4 | val illTyped = shapeless3.test.illTyped 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/com/github/dwickern/macros/JavaConstants.java: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros; 2 | 3 | public class JavaConstants { 4 | public static final int SOME_CONST = 42; 5 | 6 | public interface Inner { 7 | String INNER_CONSTANT = "value"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2") 4 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") 5 | addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.2") 6 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.4" ) 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up JDK 1.8 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: temurin 22 | java-version: 8 23 | cache: sbt 24 | 25 | - uses: sbt/setup-sbt@v1 26 | 27 | - name: Run tests 28 | run: sbt test mdoc 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [master, main] 5 | tags: ["*"] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: temurin 16 | java-version: 8 17 | cache: sbt 18 | - uses: sbt/setup-sbt@v1 19 | - run: sbt ci-release 20 | env: 21 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 22 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 23 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 24 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 25 | -------------------------------------------------------------------------------- /src/test/scala-3/thirdparty/CustomDslTest.scala: -------------------------------------------------------------------------------- 1 | package thirdparty 2 | 3 | import com.github.dwickern.macros.NameOfImpl 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | object CustomDsl { 8 | inline def ref(inline expr: Any): String = ${NameOfImpl.nameOf('expr)} 9 | } 10 | 11 | class CustomDslTest extends AnyFunSuite with Matchers { 12 | test("create alias from third-party code") { 13 | def someIdentifier = ??? 14 | CustomDsl.ref(someIdentifier) should equal ("someIdentifier") 15 | } 16 | 17 | test("compile-time constant") { 18 | CustomDsl.ref(Byte.MaxValue) should equal ("MaxValue") 19 | 20 | import CustomDsl._ 21 | ref(Byte.MaxValue) should equal ("MaxValue") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/scala-2/thirdparty/CustomDslTest.scala: -------------------------------------------------------------------------------- 1 | package thirdparty 2 | 3 | import com.github.dwickern.macros.NameOfImpl 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | object CustomDsl { 8 | import language.experimental.macros 9 | def ref(expr: Any): String = macro NameOfImpl.nameOf 10 | } 11 | 12 | class CustomDslTest extends AnyFunSuite with Matchers { 13 | test("create alias from third-party code") { 14 | def someIdentifier = ??? 15 | CustomDsl.ref(someIdentifier) should equal ("someIdentifier") 16 | } 17 | 18 | test("compile-time constant") { 19 | CustomDsl.ref(Byte.MaxValue) should equal ("MaxValue") 20 | 21 | import CustomDsl._ 22 | ref(Byte.MaxValue) should equal ("MaxValue") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Derek Wickern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/scala-3/com/github/dwickern/macros/Scala3FeaturesTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | import shapeless3.test.illTyped 6 | 7 | enum MyEnum { 8 | case EnumValue 9 | } 10 | 11 | class Scala3FeaturesTest extends AnyFunSuite with Matchers { 12 | import NameOf._ 13 | 14 | test("enum") { 15 | nameOf(MyEnum) should equal ("MyEnum") 16 | nameOfType[MyEnum] should equal ("MyEnum") 17 | nameOfType[MyEnum.EnumValue.type] should equal ("MyEnum") 18 | qualifiedNameOfType[MyEnum] should equal ("com.github.dwickern.macros.MyEnum") 19 | } 20 | 21 | test("enum value") { 22 | nameOf(MyEnum.EnumValue) should equal ("EnumValue") 23 | } 24 | 25 | test("intersection type") { 26 | trait Foo 27 | trait Bar 28 | illTyped(""" nameOfType[Foo & Bar] """) 29 | illTyped(""" qualifiedNameOfType[Foo & Bar] """) 30 | } 31 | 32 | test("union type") { 33 | trait Foo 34 | trait Bar 35 | illTyped(""" nameOfType[Foo | Bar] """) 36 | illTyped(""" qualifiedNameOfType[Foo | Bar] """) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/com/github/dwickern/macros/ConstantsTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import com.github.dwickern.macros.NameOf._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ConstantsTest extends AnyFunSuite with Matchers { 8 | 9 | test("user-defined constants, qualified") { 10 | nameOf(JavaConstants.SOME_CONST) should equal ("SOME_CONST") 11 | nameOf(JavaConstants.`SOME_CONST`) should equal ("SOME_CONST") 12 | nameOf(JavaConstants.Inner.INNER_CONSTANT) should equal ("INNER_CONSTANT") 13 | nameOf(com.github.dwickern.macros.JavaConstants.Inner.INNER_CONSTANT) should equal ("INNER_CONSTANT") 14 | 15 | import com.github.dwickern.macros.{JavaConstants => Aliased} 16 | nameOf(Aliased.SOME_CONST) should equal ("SOME_CONST") 17 | 18 | import JavaConstants._ 19 | nameOf(Inner.INNER_CONSTANT) should equal ("INNER_CONSTANT") 20 | } 21 | 22 | test("user-defined constants, unqualified") { 23 | import JavaConstants._ 24 | nameOf(SOME_CONST) should equal ("SOME_CONST") 25 | 26 | import JavaConstants.Inner._ 27 | nameOf(INNER_CONSTANT) should equal ("INNER_CONSTANT") 28 | } 29 | 30 | test("system constants, qualified") { 31 | nameOf(java.lang.Byte.MAX_VALUE) should equal ("MAX_VALUE") 32 | nameOf(Byte.MaxValue) should equal ("MaxValue") 33 | } 34 | 35 | test("system constants, unqualified") { 36 | import java.lang.Byte.MAX_VALUE 37 | nameOf(MAX_VALUE) should equal ("MAX_VALUE") 38 | } 39 | 40 | test("qualified invocations") { 41 | NameOf.nameOf(JavaConstants.SOME_CONST) should equal ("SOME_CONST") 42 | com.github.dwickern.macros.NameOf.nameOf(JavaConstants.SOME_CONST) should equal ("SOME_CONST") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala-2/com/github/dwickern/macros/NameOf.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | trait NameOf { 4 | import scala.language.experimental.macros 5 | 6 | /** 7 | * Obtain an identifier name as a constant string. 8 | * 9 | * Example usage: 10 | * {{{ 11 | * val amount = 5 12 | * nameOf(amount) => "amount" 13 | * }}} 14 | */ 15 | def nameOf(expr: Any): String = macro NameOfImpl.nameOf 16 | 17 | /** 18 | * Obtain an identifier name as a constant string. 19 | * 20 | * This overload can be used to access an instance method without having an instance of the type. 21 | * 22 | * Example usage: 23 | * {{{ 24 | * class Person(val name: String) 25 | * nameOf[Person](_.name) => "name" 26 | * }}} 27 | */ 28 | def nameOf[T](expr: T => Any): String = macro NameOfImpl.nameOf 29 | 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 | 44 | /** 45 | * Obtain a type's unqualified name as a constant string. 46 | * 47 | * Example usage: 48 | * {{{ 49 | * nameOfType[String] => "String" 50 | * nameOfType[fully.qualified.ClassName] => "ClassName" 51 | * }}} 52 | */ 53 | def nameOfType[T]: String = macro NameOfImpl.nameOfType[T] 54 | 55 | /** 56 | * Obtain a type's qualified name as a constant string. 57 | * 58 | * Example usage: 59 | * {{{ 60 | * nameOfType[String] => "java.lang.String" 61 | * nameOfType[fully.qualified.ClassName] => "fully.qualified.ClassName" 62 | * }}} 63 | */ 64 | def qualifiedNameOfType[T]: String = macro NameOfImpl.qualifiedNameOfType[T] 65 | } 66 | object NameOf extends NameOf 67 | -------------------------------------------------------------------------------- /src/main/scala-3/com/github/dwickern/macros/NameOf.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import scala.quoted.* 4 | 5 | trait NameOf { 6 | /** 7 | * Obtain an identifier name as a constant string. 8 | * 9 | * Example usage: 10 | * {{{ 11 | * val amount = 5 12 | * nameOf(amount) => "amount" 13 | * }}} 14 | */ 15 | transparent inline def nameOf(inline expr: Any): String = ${NameOfImpl.nameOf('expr)} 16 | 17 | /** 18 | * Obtain an identifier name as a constant string. 19 | * 20 | * This overload can be used to access an instance method without having an instance of the type. 21 | * 22 | * Example usage: 23 | * {{{ 24 | * class Person(val name: String) 25 | * nameOf[Person](_.name) => "name" 26 | * }}} 27 | */ 28 | transparent inline def nameOf[T](inline expr: T => Any): String = ${NameOfImpl.nameOf('expr)} 29 | 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 | 44 | /** 45 | * Obtain a type's unqualified name as a constant string. 46 | * 47 | * Example usage: 48 | * {{{ 49 | * nameOfType[String] => "String" 50 | * nameOfType[fully.qualified.ClassName] => "ClassName" 51 | * }}} 52 | */ 53 | transparent inline def nameOfType[T]: String = ${NameOfImpl.nameOfType[T]} 54 | 55 | /** 56 | * Obtain a type's qualified name as a constant string. 57 | * 58 | * Example usage: 59 | * {{{ 60 | * nameOfType[String] => "java.lang.String" 61 | * nameOfType[fully.qualified.ClassName] => "fully.qualified.ClassName" 62 | * }}} 63 | */ 64 | transparent inline def qualifiedNameOfType[T]: String = ${NameOfImpl.qualifiedNameOfType[T]} 65 | } 66 | object NameOf extends NameOf 67 | -------------------------------------------------------------------------------- /src/test/scalajvm/com/github/dwickern/macros/AnnotationTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import com.github.dwickern.macros.NameOf._ 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import javax.annotation.Resource 8 | 9 | /** 10 | * Arguments to Java runtime annotations must be compile-time constants. 11 | * 12 | * Blackbox macros widen the return time from `"name"` to `("name": String)`, 13 | * so in order for this code to compile we must use whitebox macros. 14 | */ 15 | class AnnotationTest extends AnyFunSuite with Matchers { 16 | test("nameOf") { 17 | def test = ??? 18 | 19 | @Resource(name = nameOf(test)) 20 | class AnnotatedClass 21 | 22 | val annotation = classOf[AnnotatedClass].getDeclaredAnnotation(classOf[Resource]) 23 | annotation.name should === ("test") 24 | } 25 | 26 | test("nameOf instance") { 27 | class SomeClass(val foobar: String) 28 | 29 | @Resource(name = nameOf[AnnotatedClass](_.classMember)) 30 | class AnnotatedClass { 31 | def classMember = ??? 32 | } 33 | 34 | val annotation = classOf[AnnotatedClass].getDeclaredAnnotation(classOf[Resource]) 35 | annotation.name should === ("classMember") 36 | } 37 | 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 | 50 | test("nameOfType") { 51 | @Resource(name = nameOfType[AnnotatedClass]) 52 | class AnnotatedClass 53 | 54 | val annotation = classOf[AnnotatedClass].getDeclaredAnnotation(classOf[Resource]) 55 | annotation.name should === ("AnnotatedClass") 56 | } 57 | 58 | test("qualifiedNameOfType") { 59 | @Resource(name = qualifiedNameOfType[AnnotatedClass]) 60 | class AnnotatedClass 61 | 62 | val annotation = classOf[AnnotatedClass].getDeclaredAnnotation(classOf[Resource]) 63 | annotation.name should === ("com.github.dwickern.macros.AnnotationTest.AnnotatedClass") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala-3/com/github/dwickern/macros/NameOfImpl.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import scala.annotation.tailrec 4 | import scala.quoted.* 5 | 6 | object NameOfImpl { 7 | def nameOf(expr: Expr[Any])(using Quotes): Expr[String] = { 8 | import quotes.reflect.* 9 | @tailrec def extract(tree: Tree): String = tree match { 10 | case Ident(name) => name 11 | case Select(_, name) => name 12 | case DefDef("$anonfun", _, _, Some(term)) => extract(term) 13 | case Block(List(stmt), _) => extract(stmt) 14 | case Block(_, term) => extract(term) 15 | case Apply(term, _) if term.symbol.fullName != ".throw" => extract(term) 16 | case TypeApply(term, _) => extract(term) 17 | case Inlined(_, _, term) => extract(term) 18 | case Typed(term, _) => extract(term) 19 | case _ => throw new MatchError(s"Unsupported expression: ${expr.show}") 20 | } 21 | val name = extract(expr.asTerm) 22 | Expr(name) 23 | } 24 | 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 != ".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 | 43 | def nameOfType[T](using Quotes, Type[T]): Expr[String] = { 44 | import quotes.reflect.* 45 | val name = TypeTree.of[T].tpe.dealias match { 46 | case t @ (AndType(_, _) | OrType(_, _)) => throw new MatchError(s"Unsupported type: ${t.show}") 47 | case t => t.typeSymbol.name 48 | } 49 | val clean = name.stripSuffix("$") 50 | Expr(clean) 51 | } 52 | 53 | def qualifiedNameOfType[T](using Quotes, Type[T]): Expr[String] = { 54 | import quotes.reflect.* 55 | val fullName = TypeTree.of[T].tpe.dealias match { 56 | case t @ (AndType(_, _) | OrType(_, _)) => throw new MatchError(s"Unsupported type: ${t.show}") 57 | case t => t.typeSymbol.fullName 58 | } 59 | val clean = fullName 60 | .split('.') 61 | .map(_.stripPrefix("_$").stripSuffix("$")) 62 | .mkString(".") 63 | Expr(clean) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala-2/com/github/dwickern/macros/NameOfImpl.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.whitebox 5 | import scala.annotation.tailrec 6 | 7 | object NameOfImpl { 8 | def nameOf(c: whitebox.Context)(expr: c.Expr[Any]): c.Expr[String] = { 9 | import c.universe._ 10 | 11 | @tailrec def extract(tree: c.Tree): String = tree match { 12 | case Ident(n) => n.decodedName.toString 13 | case Select(_, n) => n.decodedName.toString 14 | case Function(_, body) => extract(body) 15 | case Block(_, expr) => extract(expr) 16 | case Apply(func, _) => extract(func) 17 | case TypeApply(func, _) => extract(func) 18 | case _ => 19 | c.abort(c.enclosingPosition, s"Unsupported expression: ${expr.tree}") 20 | } 21 | 22 | /** 23 | * Compile-time constants have already been replaced before this macro runs. 24 | * For example, when calling `nameOf(Byte.MaxValue)`, the macro will see `nameOf(127)`. 25 | * We use the compiler APIs to solve this problem. 26 | */ 27 | def nameOfConstant(): String = { 28 | val cc = c.asInstanceOf[reflect.macros.runtime.Context] 29 | import cc.universe._ 30 | val macroName = cc.macroApplication.symbol.asTerm.name 31 | 32 | @tailrec def extractConstant(tree: cc.Tree): cc.Name = tree match { 33 | case Apply(RefTree(_, `macroName`), List(RefTree(_, name))) => name 34 | case Apply(func, _) => extractConstant(func) 35 | case Select(qualifier, _) => extractConstant(qualifier) 36 | case _ => 37 | c.abort(c.enclosingPosition, s"Unsupported constant expression: ${expr.tree}") 38 | } 39 | 40 | extractConstant(cc.callsiteTyper.context.tree).decodedName.toString 41 | } 42 | 43 | val name = expr.tree match { 44 | case Literal(Constant(_)) => nameOfConstant() 45 | case _ => extract(expr.tree) 46 | } 47 | c.Expr[String](q"$name") 48 | } 49 | 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 | 70 | def nameOfType[T](c: whitebox.Context)(implicit tag: c.WeakTypeTag[T]): c.Expr[String] = { 71 | import c.universe._ 72 | val name = showRaw(tag.tpe.typeSymbol.name) 73 | c.Expr[String](q"$name") 74 | } 75 | 76 | def qualifiedNameOfType[T](c: whitebox.Context)(implicit tag: c.WeakTypeTag[T]): c.Expr[String] = { 77 | import c.universe._ 78 | val name = showRaw(tag.tpe.typeSymbol.fullName) 79 | c.Expr[String](q"$name") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | "nameOf" macro for scala 2 | ======================== 3 | 4 | [![Build](https://github.com/dwickern/scala-nameof/workflows/build/badge.svg)](https://github.com/dwickern/scala-nameof/actions) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.dwickern/scala-nameof_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.dwickern/scala-nameof_2.13) 6 | [![javadoc](https://javadoc.io/badge2/com.github.dwickern/scala-nameof_2.13/javadoc.svg)](https://javadoc.io/doc/com.github.dwickern/scala-nameof_2.13/latest/com/github/dwickern/macros/NameOf.html) 7 | 8 | Get the name of an variable, function, class member, or type as a string--at compile-time! 9 | 10 | Inspired by the [nameof](https://msdn.microsoft.com/en-us/library/dn986596.aspx) operator in C# 11 | 12 | > Used to obtain the simple (unqualified) string name of a variable, type, or member. When reporting errors in code, hooking up model-view-controller (MVC) links, firing property changed events, etc., you often want to capture the string name of a method. Using nameof helps keep your code valid when renaming definitions. Before you had to use string literals to refer to definitions, which is brittle when renaming code elements because tools do not know to check these string literals. 13 | 14 | Usage 15 | ===== 16 | 17 | Add the library as "provided", because it's only needed during compilation and not at runtime: 18 | ```sbt 19 | libraryDependencies += "com.github.dwickern" %% "scala-nameof" % "4.0.0" % "provided" 20 | ``` 21 | 22 | And import the package: 23 | ```scala mdoc 24 | import com.github.dwickern.macros.NameOf._ 25 | ``` 26 | 27 | Now you can use `nameOf` to get the name of a variable or class member: 28 | ```scala mdoc:nest 29 | case class Person(name: String, age: Int) 30 | 31 | def toMap(person: Person) = Map( 32 | nameOf(person.name) -> person.name, 33 | nameOf(person.age) -> person.age 34 | ) 35 | ``` 36 | ```scala mdoc:nest 37 | // compiles to: 38 | 39 | def toMap(person: Person) = Map( 40 | "name" -> person.name, 41 | "age" -> person.age 42 | ) 43 | ``` 44 | 45 | To get the name of a function: 46 | ```scala mdoc:nest 47 | def startCalculation(value: Int): Unit = { 48 | println("Entered " + nameOf(startCalculation _)) 49 | } 50 | ``` 51 | ```scala mdoc:nest 52 | // compiles to: 53 | 54 | def startCalculation(value: Int): Unit = { 55 | println("Entered startCalculation") 56 | } 57 | ``` 58 | 59 | Without having an instance of the type: 60 | ```scala mdoc:nest 61 | case class Person(name: String, age: Int) { 62 | def sayHello(other: Person) = s"Hello ${other.name}!" 63 | } 64 | 65 | println(nameOf[Person](_.age)) 66 | println(nameOf[Person](_.sayHello(???))) 67 | ``` 68 | ```scala mdoc:nest 69 | // compiles to: 70 | 71 | println("age") 72 | println("sayHello") 73 | ``` 74 | 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 | 88 | You can also use `nameOfType` to get the unqualified name of a type: 89 | ```scala mdoc:nest 90 | println(nameOfType[java.lang.String]) 91 | ``` 92 | ```scala mdoc:nest 93 | // compiles to: 94 | 95 | println("String") 96 | ``` 97 | 98 | And `qualifiedNameOfType` to get the qualified name: 99 | ```scala mdoc:nest 100 | println(qualifiedNameOfType[java.lang.String]) 101 | ``` 102 | ```scala mdoc:nest 103 | // compiles to: 104 | 105 | println("java.lang.String") 106 | ``` 107 | 108 | 109 | Development 110 | =========== 111 | 112 | To run tests for all compilation targets: 113 | 114 | sbt +test 115 | 116 | To publish to your local ivy repository: 117 | 118 | sbt +publishLocal 119 | 120 | To publish to maven central (requires authorization): 121 | 122 | sbt release 123 | 124 | 125 | License 126 | ======= 127 | 128 | See [LICENSE](LICENSE.md) (MIT). 129 | -------------------------------------------------------------------------------- /src/test/scala/com/github/dwickern/macros/NameOfTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.dwickern.macros 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import test.illTyped 7 | 8 | class RegularClass() 9 | case class CaseClass() 10 | object SomeObject 11 | object OuterObject { 12 | class InnerClass() 13 | } 14 | class OuterClass { 15 | class InnerClass() 16 | } 17 | class GenericClass[T, U]() 18 | class ClassWithCompanion 19 | object ClassWithCompanion 20 | 21 | class NameOfTest extends AnyFunSuite with Matchers { 22 | import NameOf._ 23 | 24 | test("def") { 25 | def localDef = ??? 26 | nameOf(localDef) should equal ("localDef") 27 | } 28 | 29 | test("def()") { 30 | def localDef() = ??? 31 | nameOf(localDef()) should equal ("localDef") 32 | } 33 | 34 | test("val") { 35 | val localVal = "" 36 | nameOf(localVal) should equal ("localVal") 37 | } 38 | 39 | test("symbolic names") { 40 | nameOf(???) should equal ("???") 41 | 42 | def `multi word name` = ??? 43 | nameOf(`multi word name`) should equal ("multi word name") 44 | 45 | def 你好 = ??? 46 | nameOf(你好) should equal ("你好") 47 | 48 | def `!@#$%^&*` = ??? 49 | nameOf(`!@#$%^&*`) should equal ("!@#$%^&*") 50 | } 51 | 52 | test("function") { 53 | def func1(x: Int): String = ??? 54 | nameOf(func1 _) should equal ("func1") 55 | nameOf(func1(???)) should equal ("func1") 56 | 57 | def func2(x: Int, y: Int): String = ??? 58 | nameOf(func2 _) should equal ("func2") 59 | nameOf(func2(???, ???)) should equal ("func2") 60 | 61 | def func3(x: Double, y: Int, z: String): Boolean = ??? 62 | nameOf(func3 _) should equal ("func3") 63 | nameOf(func3(???, ???, ???)) should equal ("func3") 64 | } 65 | 66 | test("curried function") { 67 | def curried(x: Int)(y: Int): String = ??? 68 | nameOf(curried _) should equal ("curried") 69 | } 70 | 71 | test("generic function") { 72 | def generic[T](x: Int): String = ??? 73 | nameOf(generic _) should equal ("generic") 74 | nameOf(generic(???)) should equal ("generic") 75 | } 76 | 77 | test("identity function") { 78 | nameOf[String](x => x) should equal ("x") 79 | qualifiedNameOf[String](x => x) should equal ("") 80 | } 81 | 82 | test("instance member") { 83 | class SomeClass(val foobar: String) 84 | val myClass = new SomeClass("") 85 | nameOf(myClass.foobar) should equal ("foobar") 86 | } 87 | 88 | test("this member") { 89 | new { 90 | private[this] val foobar = "" 91 | nameOf(this.foobar) should equal ("foobar") 92 | } 93 | } 94 | 95 | test("class member") { 96 | class SomeClass(val foobar: String) 97 | nameOf[SomeClass](_.foobar) should equal ("foobar") 98 | nameOf { (c: SomeClass) => c.foobar } should equal ("foobar") 99 | nameOf((_: SomeClass).foobar) should equal ("foobar") 100 | qualifiedNameOf[SomeClass](_.foobar) should equal ("foobar") 101 | } 102 | 103 | test("object member") { 104 | object SomeObject { 105 | lazy val member = ??? 106 | } 107 | nameOf(SomeObject.member) should equal ("member") 108 | qualifiedNameOf[SomeObject.type](_.member) should equal ("member") 109 | } 110 | 111 | test("class") { 112 | nameOfType[RegularClass] should equal ("RegularClass") 113 | qualifiedNameOfType[RegularClass] should equal ("com.github.dwickern.macros.RegularClass") 114 | } 115 | 116 | test("case class") { 117 | nameOf(CaseClass) should equal ("CaseClass") 118 | nameOfType[CaseClass] should equal ("CaseClass") 119 | 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 | } 163 | 164 | qualifiedNameOf[C1](_.c2.c3.func _) should equal ("c2.c3.func") 165 | qualifiedNameOf[C1](_.c2.c3.func(???)) should equal ("c2.c3.func") 166 | } 167 | 168 | test("object") { 169 | nameOf(SomeObject) should equal ("SomeObject") 170 | nameOfType[SomeObject.type] should equal ("SomeObject") 171 | qualifiedNameOfType[SomeObject.type] should equal ("com.github.dwickern.macros.SomeObject") 172 | } 173 | 174 | test("class with companion object") { 175 | nameOfType[ClassWithCompanion] should equal ("ClassWithCompanion") 176 | nameOfType[ClassWithCompanion.type] should equal ("ClassWithCompanion") 177 | qualifiedNameOfType[ClassWithCompanion] should equal ("com.github.dwickern.macros.ClassWithCompanion") 178 | qualifiedNameOfType[ClassWithCompanion.type] should equal ("com.github.dwickern.macros.ClassWithCompanion") 179 | } 180 | 181 | test("object/class") { 182 | nameOfType[OuterObject.InnerClass] should equal ("InnerClass") 183 | qualifiedNameOfType[OuterObject.InnerClass] should equal ("com.github.dwickern.macros.OuterObject.InnerClass") 184 | } 185 | 186 | test("class/class") { 187 | nameOfType[OuterClass#InnerClass] should equal ("InnerClass") 188 | qualifiedNameOfType[OuterClass#InnerClass] should equal ("com.github.dwickern.macros.OuterClass.InnerClass") 189 | } 190 | 191 | test("function/class") { 192 | class Foo 193 | nameOfType[Foo] should equal ("Foo") 194 | qualifiedNameOfType[Foo] should equal ("com.github.dwickern.macros.NameOfTest.Foo") 195 | } 196 | 197 | test("function/object") { 198 | object Foo 199 | nameOfType[Foo.type] should equal ("Foo") 200 | qualifiedNameOfType[Foo.type] should equal ("com.github.dwickern.macros.NameOfTest.Foo") 201 | } 202 | 203 | test("generic class") { 204 | nameOfType[GenericClass[_, _]] should equal ("GenericClass") 205 | qualifiedNameOfType[GenericClass[_, _]] should equal ("com.github.dwickern.macros.GenericClass") 206 | } 207 | 208 | test("primitive type") { 209 | nameOfType[Int] should equal ("Int") 210 | qualifiedNameOfType[Int] should equal ("scala.Int") 211 | } 212 | 213 | test("java type") { 214 | nameOfType[java.lang.String] should equal ("String") 215 | qualifiedNameOfType[java.lang.String] should equal ("java.lang.String") 216 | } 217 | 218 | test("type alias") { 219 | type alias = String 220 | nameOfType[alias] should equal ("String") 221 | qualifiedNameOfType[alias] should equal ("java.lang.String") 222 | } 223 | 224 | test("error: this") { 225 | illTyped(""" nameOf(this) """, "Unsupported expression: this") 226 | } 227 | 228 | test("error: throw") { 229 | illTyped(""" nameOf(throw new Exception("test")) """, 230 | """Unsupported expression: throw new scala\.`package`\.Exception\("test"\)""") 231 | } 232 | 233 | test("error: nested") { 234 | def foo = ??? 235 | illTyped(""" nameOf(nameOf(foo)) """, "Unsupported constant expression: \"foo\"") 236 | } 237 | 238 | test("error: literals") { 239 | illTyped(""" nameOf("test") """, "Unsupported constant expression: \"test\"") 240 | illTyped(""" nameOf(123) """, "Unsupported constant expression: 123") 241 | illTyped(""" nameOf(true) """, "Unsupported constant expression: true") 242 | illTyped(""" nameOf(null) """, "Unsupported constant expression: null") 243 | illTyped(""" nameOf() """, "Unsupported constant expression: \\(\\)") 244 | illTyped(""" qualifiedNameOf[String](_ => ()) """) 245 | illTyped(""" qualifiedNameOf[String](_ => 3) """) 246 | } 247 | } 248 | --------------------------------------------------------------------------------