From 994c99546331541253d761284bd7a2058deffed0 Mon Sep 17 00:00:00 2001
From: Hamza Remmal <hamza@remmal.dev>
Date: Tue, 15 Oct 2024 11:43:07 +0200
Subject: [PATCH 1/6] First draft of the @toString annotation

---
 .../scala/steps/annotation/toString.scala     | 41 +++++++++++++++++++
 1 file changed, 41 insertions(+)
 create mode 100644 src/main/scala/steps/annotation/toString.scala

diff --git a/src/main/scala/steps/annotation/toString.scala b/src/main/scala/steps/annotation/toString.scala
new file mode 100644
index 0000000..39668ce
--- /dev/null
+++ b/src/main/scala/steps/annotation/toString.scala
@@ -0,0 +1,41 @@
+package steps.annotation
+
+import scala.annotation.{experimental, MacroAnnotation}
+import scala.quoted.*
+
+@experimental
+final class toString extends MacroAnnotation:
+  override def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] = 
+    import quotes.reflect.*
+    val toStringSym = Symbol.requiredMethod("java.lang.Object.toString")
+    tree match
+      case _: ClassDef if toStringSym.overridingSymbol(tree.symbol).exists =>
+        report.warning(s"@toString is not necessary since hashcode is defined in ${tree.symbol}")
+        List(tree)
+      case ClassDef(className, ctr, parents, self, body) =>
+        val cls = tree.symbol
+
+        val fields = body.collect {
+          case vdef: ValDef if vdef.symbol.flags.is(Flags.ParamAccessor) =>
+            Select(This(cls), vdef.symbol).asExpr
+        }
+
+        val toStringOverrideSym = Symbol.newMethod(cls, "toString", toStringSym.info, Flags.Override, Symbol.noSymbol)
+
+        def toStringOverrideDefBody(argss: List[List[Tree]]): Option[Term] =
+          given Quotes = toStringOverrideSym.asQuotes
+          Some(toStringExpr(className, fields).asTerm)
+
+        val toStringDef = DefDef(toStringOverrideSym, toStringOverrideDefBody)
+        List(ClassDef.copy(tree)(className, ctr, parents, self, toStringDef :: body))
+      case _ =>
+        report.errorAndAbort("@toString is only supported in class/object/trait")
+  end transform
+
+  private def toStringExpr(className: String, thisFields: List[Expr[Any]])(using Quotes): Expr[String] =
+    val fieldsSeq = Expr.ofSeq(thisFields)
+    val prefix = Expr(className + "(")
+    '{ $fieldsSeq.mkString($prefix, ", ", ")") }
+  end toStringExpr
+
+end toString
\ No newline at end of file

From 2f0862df026ef6694eba6d931c740a8a261210fa Mon Sep 17 00:00:00 2001
From: Hamza Remmal <hamza@remmal.dev>
Date: Tue, 15 Oct 2024 13:00:31 +0200
Subject: [PATCH 2/6] Restrict toString to classes only

---
 src/main/scala/steps/annotation/toString.scala | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/main/scala/steps/annotation/toString.scala b/src/main/scala/steps/annotation/toString.scala
index 39668ce..ce72ab0 100644
--- a/src/main/scala/steps/annotation/toString.scala
+++ b/src/main/scala/steps/annotation/toString.scala
@@ -12,6 +12,12 @@ final class toString extends MacroAnnotation:
       case _: ClassDef if toStringSym.overridingSymbol(tree.symbol).exists =>
         report.warning(s"@toString is not necessary since hashcode is defined in ${tree.symbol}")
         List(tree)
+      case cls: ClassDef if cls.symbol.flags.is(Flags.Trait) =>
+        report.error(s"@toString is not supported in traits")
+        List(tree)
+      case cls: ClassDef if cls.symbol.flags.is(Flags.Module) =>
+        report.error(s"@toString is not supported in object")
+        List(tree)
       case ClassDef(className, ctr, parents, self, body) =>
         val cls = tree.symbol
 
@@ -29,7 +35,7 @@ final class toString extends MacroAnnotation:
         val toStringDef = DefDef(toStringOverrideSym, toStringOverrideDefBody)
         List(ClassDef.copy(tree)(className, ctr, parents, self, toStringDef :: body))
       case _ =>
-        report.errorAndAbort("@toString is only supported in class/object/trait")
+        report.errorAndAbort("@toString is only supported in class")
   end transform
 
   private def toStringExpr(className: String, thisFields: List[Expr[Any]])(using Quotes): Expr[String] =

From 8bbb7e5cc3cc1bf070d753c57aa042cf55c27911 Mon Sep 17 00:00:00 2001
From: Hamza Remmal <hamza@remmal.dev>
Date: Tue, 15 Oct 2024 13:01:40 +0200
Subject: [PATCH 3/6] Add scaladoc for steps.annotation.toString

---
 .../scala/steps/annotation/toString.scala     | 45 +++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/src/main/scala/steps/annotation/toString.scala b/src/main/scala/steps/annotation/toString.scala
index ce72ab0..07d5cf4 100644
--- a/src/main/scala/steps/annotation/toString.scala
+++ b/src/main/scala/steps/annotation/toString.scala
@@ -3,8 +3,44 @@ package steps.annotation
 import scala.annotation.{experimental, MacroAnnotation}
 import scala.quoted.*
 
+
+/**
+ * A macro annotation that automatically generates a custom `toString` method for a class.
+ * 
+ * The `@toString` annotation can be applied to a class, object, or trait. When applied, it overrides the `toString`
+ * method to include the class name and the values of all fields marked as `ParamAccessor`.
+ * 
+ * If the class already defines or overrides the `toString` method, the annotation will emit a warning indicating that
+ * the annotation is not necessary. The existing `toString` method will remain unchanged.
+ *
+ * Example usage:
+ * {{{
+ * @toString
+ * class MyClass(val a: Int, val b: String)
+ *
+ * val instance = MyClass(1, "hello")
+ * println(instance.toString) // Output: MyClass(1, hello)
+ * }}}
+ *
+ * The generated `toString` method produces output in the format: `ClassName(field1, field2, ...)`.
+ *
+ * @note This annotation requires Scala's `experimental` flag to be enabled, or it can be used within a scope
+ *       marked as experimental (i.e., using `import scala.annotation.experimental`). This is necessary because it 
+ *       relies on experimental macro annotation features.
+ */
 @experimental
 final class toString extends MacroAnnotation:
+
+  /**
+   * Transforms the annotated class to add a custom `toString` method.
+   * 
+   * If the class already overrides `toString`, a warning is emitted and no changes are made.
+   * Otherwise, this annotation adds a new `toString` method that returns a string representation of
+   * the class name and its fields.
+   * 
+   * @param tree The abstract syntax tree (AST) of the annotated class, object, or trait.
+   * @return The transformed class definition, with the generated `toString` method if applicable.
+   */
   override def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] = 
     import quotes.reflect.*
     val toStringSym = Symbol.requiredMethod("java.lang.Object.toString")
@@ -38,6 +74,15 @@ final class toString extends MacroAnnotation:
         report.errorAndAbort("@toString is only supported in class")
   end transform
 
+  /**
+   * Helper method to create the string representation of the class.
+   *
+   * Constructs a string in the format: `ClassName(field1, field2, ...)`.
+   *
+   * @param className The name of the class.
+   * @param thisFields The list of expressions representing the class fields.
+   * @return A quoted expression representing the final string.
+   */
   private def toStringExpr(className: String, thisFields: List[Expr[Any]])(using Quotes): Expr[String] =
     val fieldsSeq = Expr.ofSeq(thisFields)
     val prefix = Expr(className + "(")

From a9cc7833e2bd4009c2e2a323d7e10298508ce3c7 Mon Sep 17 00:00:00 2001
From: Hamza Remmal <hamza@remmal.dev>
Date: Tue, 15 Oct 2024 13:02:03 +0200
Subject: [PATCH 4/6] Add tests for steps.annotation.toString

---
 .../scala/steps/annotation/toString.scala     | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 src/test/scala/steps/annotation/toString.scala

diff --git a/src/test/scala/steps/annotation/toString.scala b/src/test/scala/steps/annotation/toString.scala
new file mode 100644
index 0000000..d4dd521
--- /dev/null
+++ b/src/test/scala/steps/annotation/toString.scala
@@ -0,0 +1,31 @@
+package steps.annotation
+
+import scala.annotation.experimental
+import scala.language.experimental.clauseInterleaving
+
+@toString
+@experimental
+class Foo1(val a: Int, val b: String)
+
+@toString
+@experimental
+class Foo2(a: Int, b: String)
+
+@toString
+@experimental
+class Foo3(var a: Int, var b: String)
+
+@toString
+@experimental
+class Foo4(a: Int, b: String)(c: Int)
+
+@experimental
+class AssertToStringBehaviour extends munit.FunSuite:
+
+  test("@toString works with all kinds of classes"):
+    assertEquals(Foo1(1, "hello").toString(), "Foo1(1, hello)")
+    assertEquals(Foo2(1, "hello").toString(), "Foo2(1, hello)")
+    assertEquals(Foo3(1, "hello").toString(), "Foo3(1, hello)")
+    assertEquals(Foo4(1, "hello")(2).toString(), "Foo4(1, hello, 2)")
+
+end AssertToStringBehaviour
\ No newline at end of file

From 9eb4b45baa99a1fe9efaeb0dcc611d3511bf2d2a Mon Sep 17 00:00:00 2001
From: Hamza Remmal <hamza@remmal.net>
Date: Tue, 15 Oct 2024 14:18:29 +0200
Subject: [PATCH 5/6] Fix typo in warning messages

Co-authored-by: Natsu Kagami <natsukagami@gmail.com>
---
 src/main/scala/steps/annotation/toString.scala | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/steps/annotation/toString.scala b/src/main/scala/steps/annotation/toString.scala
index 07d5cf4..6928341 100644
--- a/src/main/scala/steps/annotation/toString.scala
+++ b/src/main/scala/steps/annotation/toString.scala
@@ -46,7 +46,7 @@ final class toString extends MacroAnnotation:
     val toStringSym = Symbol.requiredMethod("java.lang.Object.toString")
     tree match
       case _: ClassDef if toStringSym.overridingSymbol(tree.symbol).exists =>
-        report.warning(s"@toString is not necessary since hashcode is defined in ${tree.symbol}")
+        report.warning(s"@toString is not necessary since toString is defined in ${tree.symbol}")
         List(tree)
       case cls: ClassDef if cls.symbol.flags.is(Flags.Trait) =>
         report.error(s"@toString is not supported in traits")

From 99e550556fe4343987d45c2ec8a3f48b3aba9577 Mon Sep 17 00:00:00 2001
From: Hamza Remmal <hamza@remmal.net>
Date: Tue, 15 Oct 2024 14:21:53 +0200
Subject: [PATCH 6/6] Apply suggestions from code review

Co-authored-by: Natsu Kagami <natsukagami@gmail.com>
---
 src/main/scala/steps/annotation/toString.scala | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/scala/steps/annotation/toString.scala b/src/main/scala/steps/annotation/toString.scala
index 6928341..5f961cc 100644
--- a/src/main/scala/steps/annotation/toString.scala
+++ b/src/main/scala/steps/annotation/toString.scala
@@ -49,10 +49,10 @@ final class toString extends MacroAnnotation:
         report.warning(s"@toString is not necessary since toString is defined in ${tree.symbol}")
         List(tree)
       case cls: ClassDef if cls.symbol.flags.is(Flags.Trait) =>
-        report.error(s"@toString is not supported in traits")
+        report.error(s"@toString is not supported on traits")
         List(tree)
       case cls: ClassDef if cls.symbol.flags.is(Flags.Module) =>
-        report.error(s"@toString is not supported in object")
+        report.error(s"@toString is not supported on objects")
         List(tree)
       case ClassDef(className, ctr, parents, self, body) =>
         val cls = tree.symbol
@@ -62,7 +62,7 @@ final class toString extends MacroAnnotation:
             Select(This(cls), vdef.symbol).asExpr
         }
 
-        val toStringOverrideSym = Symbol.newMethod(cls, "toString", toStringSym.info, Flags.Override, Symbol.noSymbol)
+        val toStringOverrideSym = Symbol.newMethod(cls, toStringSym.name, toStringSym.info, Flags.Override, Symbol.noSymbol)
 
         def toStringOverrideDefBody(argss: List[List[Tree]]): Option[Term] =
           given Quotes = toStringOverrideSym.asQuotes
@@ -71,7 +71,7 @@ final class toString extends MacroAnnotation:
         val toStringDef = DefDef(toStringOverrideSym, toStringOverrideDefBody)
         List(ClassDef.copy(tree)(className, ctr, parents, self, toStringDef :: body))
       case _ =>
-        report.errorAndAbort("@toString is only supported in class")
+        report.errorAndAbort("@toString is only supported on class")
   end transform
 
   /**