Is there a way to create custom annotations in Scala and write a custom annotation processor to verify the annotations?
Asked Answered
H

2

7

I have been learning about annotations and what an annotation processor is. I was looking at Java examples and there seems to be a proper way to do it. However, in Scala, I do not get a proper website/documentation to create custom annotations and annotation processor.

If it's not possible in Scala, is there a way to use Java custom annotation processor in Scala classes?

Can someone point me in the right direction?

Houphouetboigny answered 18/10, 2019 at 2:13 Comment(1)
I added an example how to handle both Java and Scala sources.Eightieth
E
2

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
Eightieth answered 18/10, 2019 at 7:35 Comment(6)
Thanks. but I want to know write an annotation processor in Java and use it on Scala. Is the linking possible?Houphouetboigny
@user7937993 Annotation processing in Java is executed by javac. How can scalac be aware of that? Scalac is aware of macro annotations, macros and compiler plugins instead.Eightieth
I can understand that. My question is based on fundamental concepts.I am not talking about macros. Scala supports user defined annotations and it does Java classes too. So my thoughts are like 1) How Scala checks for annotations? What processor does it have? 2) Since scala supports Java classes, Is there a way to extend AbstractProcessor in scala and write custom annotation processor?Houphouetboigny
@Houphouetboigny 1) It doesn't have any processors. Scala macro annotations and Java annotation processors are two different mechanisms doing similar things. 2) Yes, see my answer.Eightieth
reddit.com/r/scala/comments/11wb92z/…Eightieth
kotlinlang.org/docs/kapt.htmlEightieth
O
1

In Scala there is an option to use compile-time annotations in black-box (type-safe) macros without need to add a compiler extensions or flags.

Here is an example of defining and usage of such annotations for derivation of codecs.

Orgasm answered 18/10, 2019 at 8:0 Comment(1)
Thanks. but I want to know write an annotation processor in Java and use it on Scala. Is the linking possible?Houphouetboigny

© 2022 - 2024 — McMap. All rights reserved.