In Scala there are macro annotations
https://docs.scala-lang.org/overviews/macros/annotations.html
I guess this is similar to compile-time processing annotations in Java.
An annotation processor can be written in Scala. But the annotation must be written in Java (scala annotations can't annotate Java code). And the annotation processor will not handle Scala sources. Java compile-time annotation processing is handled by Java compiler, it can't compile Scala sources.
Scala compiler is not aware of any annotation processors.
In Scala compile-time annotation processing is macro annotations (similarly they can handle Scala sources, not Java sources). Scala macro annotations and Java annotation processors are two completely different mechanisms doing similar things with Scala sources and Java sources coordingly.
So if you want to process Java and Scala sources similarly you'll have to duplicate efforts. You'll have to create annotation processor handling Java sources and macro annotation doing similar thing to Scala sources.
Here is an example with creating a builder. The annotation processor creates a builder in target/scala-2.13/classes
, the macro annotation creates a builder inside a companion object. This is a difference between processors and macro annotations: processors can generate code but not re-write it (without Java compiler internals 1 2), macro annotations can re-write code but only in a class and its companion. One more difference is that the processors generate Java sources while macro annotations generate Scala ASTs.
annotation-processor/src/main/java/org/example/BuilderProperty.java
package org.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}
annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
org.example.BuilderProcessor
annotation-processor/src/main/scala/org/example/BuilderProcessor.scala
package org.example
//import com.google.auto.service.AutoService
import javax.annotation.processing._
import javax.lang.model.SourceVersion
import javax.lang.model.element.{Element, TypeElement}
import javax.lang.model.`type`.ExecutableType
import javax.tools.Diagnostic
import java.io.IOException
import java.io.PrintWriter
import java.util
import scala.collection.immutable
import scala.jdk.CollectionConverters._
import scala.util.Using
@SupportedAnnotationTypes(Array("org.example.BuilderProperty"))
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//@AutoService(Array(classOf[Processor])) // can't use AutoService because the processor is written in Scala, so using the file in META-INF
class BuilderProcessor extends AbstractProcessor {
override def process(annotations: util.Set[_ <: TypeElement], roundEnv: RoundEnvironment): Boolean = {
System.out.println("process")
// println("process") // java.lang.RuntimeException: java.lang.NoSuchMethodError: scala.Predef$.println(Ljava/lang/Object;)V //com.sun.tools.javac.main.Main.compile //sbt.internal.inc.javac.LocalJavaCompiler.run
// annotations.asScala.toSet[TypeElement].foreach { annotation => //java.lang.RuntimeException: java.lang.NoSuchMethodError: scala.jdk.CollectionConverters$.SetHasAsScala(Ljava/util/Set;)Lscala/collection/convert/AsScalaExtensions$SetHasAsScala;
new SetHasAsScala(annotations).asScala.toSet[TypeElement].foreach { annotation =>
val annotatedElements = roundEnv.getElementsAnnotatedWith(annotation)
val (setters: Set[Element @unchecked], otherMethods) = new SetHasAsScala(annotatedElements).asScala.toSet.partition(element =>
element.asType.asInstanceOf[ExecutableType].getParameterTypes.size == 1 &&
element.getSimpleName.toString.startsWith("set")
)
otherMethods.foreach(element =>
processingEnv.getMessager.printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty must be applied to a setXxx method with a single argument", element)
)
setters.headOption.foreach { head =>
val className = head.getEnclosingElement.asInstanceOf[TypeElement].getQualifiedName.toString
val setterMap = setters.map(setter =>
setter.getSimpleName.toString -> setter.asType.asInstanceOf[ExecutableType].getParameterTypes.get(0).toString
)
writeBuilderFile(className, setterMap)
}
}
true
}
@throws[IOException]
private def writeBuilderFile(className: String, setterMap: immutable.Set[(String, String)]): Unit = {
val lastDot = className.lastIndexOf('.')
val packageName = if (lastDot > 0) Some(className.substring(0, lastDot)) else None
// val packageName = Option.when(lastDot > 0)(className.substring(0, lastDot)) //java.lang.RuntimeException: java.lang.NoSuchMethodError: scala.Option$.when(ZLscala/Function0;)Lscala/Option; //com.sun.tools.javac.main.Main.compile //sbt.internal.inc.javac.LocalJavaCompiler.run
val simpleClassName = className.substring(lastDot + 1)
val builderClassName = className + "Builder"
val builderSimpleClassName = builderClassName.substring(lastDot + 1)
val builderFile = processingEnv.getFiler.createSourceFile(builderClassName)
Using(new PrintWriter(builderFile.openWriter)) { out =>
val packageStr = packageName.map(name => s"package $name;\n\n").getOrElse("")
out.print(
s"""${packageStr}public class $builderSimpleClassName {
|
| private $simpleClassName object = new $simpleClassName();
|
| public $simpleClassName build() {
| return object;
| }
|
|${ setterMap.map { case methodName -> argumentType =>
s""" public $builderSimpleClassName $methodName($argumentType value) {
| object.$methodName(value);
| return this;
| }
|""".stripMargin }.mkString("\n") }
|}
|""".stripMargin
)
}
}
}
(For some reason println
, .asScala
, and Option.when
throw NoSuchMethodError
during processing.)
annotation-processor/src/main/scala/org/example/scalaBuilderProperty.scala
package org.example
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro annotations")
class scalaBuilderProperty extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro BuilderPropertyMacro.impl
}
object BuilderPropertyMacro {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
def modifyObject(cls: Tree, obj: Tree): Tree = (cls, obj) match {
case
(
q"$_ class $tpname[..$_] $_(...$paramss) extends { ..$_ } with ..$_ { $_ => ..$_ }",
q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }"
) =>
val builder = TypeName(s"${tpname}Builder")
def isBuilderPropertyAnnotated(mods: Modifiers): Boolean = {
def removeMetaAnnotations(tpe: Type): Type = tpe match {
case tp: AnnotatedType => removeMetaAnnotations(tp.underlying)
case _ => tpe
}
def getType(tree: Tree): Type = c.typecheck(tree, mode = c.TYPEmode, silent = true).tpe
mods.annotations
.collect {
case q"new { ..$_ } with ..$parents { $_ => ..$_ }" => parents
}
.flatten
.map(t => removeMetaAnnotations(getType(t)))
.exists(_ =:= typeOf[BuilderProperty])
}
val setters = paramss.flatten.collect {
case q"$mods var $tname: $tpt = $_" if isBuilderPropertyAnnotated(mods) =>
val setter = TermName(s"set${tname.toString.capitalize}")
q"""def $setter(value: $tpt): $builder = {
this.`object`.$setter(value)
this
}"""
}
q"""$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
..$body
class $builder {
private val `object`: $tpname = new $tpname()
def build: $tpname = this.`object`
..$setters
}
}"""
}
def modify(cls: Tree, obj: Tree): Tree = q"..${Seq(cls, modifyObject(cls, obj))}"
annottees match {
case (cls: ClassDef) :: (obj: ModuleDef) :: Nil =>
modify(cls, obj)
case (cls@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>
modify(cls, q"object ${tpname.toTermName}")
case _ => c.abort(c.enclosingPosition, "@scalaBuilderProperty must annotate classes")
}
}
}
annotation-user/src/main/java/org/example/Person.java
package org.example;
public class Person {
private int age;
private String name;
public int getAge() {
return age;
}
@BuilderProperty
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
@BuilderProperty
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
annotation-user/target/scala-2.13/classes/org/example/PersonBuilder.java
// GENERATED JAVA SOURCE
//package org.example;
//
//public class PersonBuilder {
//
// private Person object = new Person();
//
// public Person build() {
// return object;
// }
//
// public PersonBuilder setAge(int value) {
// object.setAge(value);
// return this;
// }
//
// public PersonBuilder setName(java.lang.String value) {
// object.setName(value);
// return this;
// }
//
//}
annotation-user/src/main/scala/org/example/ScalaPerson.scala
package org.example
//import scala.annotation.meta.beanSetter
import scala.beans.BeanProperty
@scalaBuilderProperty
case class ScalaPerson(
@BeanProperty
@(BuilderProperty /*@beanSetter @beanSetter*/)
var age: Int = 0,
@BeanProperty
@(BuilderProperty /*@beanSetter*/)
var name: String = ""
)
// GENERATED SCALA AST (-Ymacro-debug-lite)
//object ScalaPerson extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// class ScalaPersonBuilder extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// private val `object`: ScalaPerson = new ScalaPerson();
// def build: ScalaPerson = this.`object`;
// def setAge(value: Int): ScalaPersonBuilder = {
// this.`object`.setAge(value);
// this
// };
// def setName(value: String): ScalaPersonBuilder = {
// this.`object`.setName(value);
// this
// }
// }
// };
// ()
//}
annotation-user/src/main/scala/org/example/Main.scala
package org.example
object Main {
def main(args: Array[String]): Unit = {
val person = new PersonBuilder()
.setAge(25)
.setName("John")
.build
println(person)//Person{age=25, name='John'}
val person1 = new ScalaPerson.ScalaPersonBuilder()
.setAge(25)
.setName("John")
.build
println(person1)//ScalaPerson(25,John)
}
}
build.sbt
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.10"
lazy val `annotation-processor` = project
.settings(
libraryDependencies ++= Seq(
// "com.google.auto.service" % "auto-service" % "1.0.1", //https://github.com/google/auto/tree/master/service
// "org.kohsuke.metainf-services" % "metainf-services" % "1.9", //https://github.com/kohsuke/metainf-services
scalaOrganization.value % "scala-reflect" % scalaVersion.value,
),
scalacOptions ++= Seq(
"-feature",
"-Ymacro-annotations",
),
javacOptions ++= Seq(
"-proc:none", // otherwise META-INF should be moved into annotation-processor-metainf or annotation-user
),
)
//lazy val `annotation-processor-metainf` = project
// .dependsOn(`annotation-processor`)
lazy val `annotation-user` = project
.settings(
compileOrder := CompileOrder.JavaThenScala, // can't use Scala in Java, but otherwise Main.scala should be moved into core
scalacOptions ++= Seq(
"-Ymacro-annotations",
"-Ymacro-debug-lite",
),
)
.dependsOn(`annotation-processor`)
// .dependsOn(`annotation-processor-metainf`)
//lazy val core = project
// .dependsOn(`annotation-user`)
sbt clean compile annotation-user/run