From 48453537c90278e366c8ae17ab0398ac92d20854 Mon Sep 17 00:00:00 2001
From: Nicolas Stucki <nicolas.stucki@gmail.com>
Date: Mon, 11 Oct 2021 11:34:14 +0200
Subject: [PATCH] Add reflect `ClassDef.apply` and `Symbol.newClass`

---
 .../src/dotty/tools/dotc/core/Symbols.scala   |  2 +-
 .../quoted/runtime/impl/QuotesImpl.scala      | 24 +++++-
 library/src/scala/quoted/Quotes.scala         | 85 +++++++++++++++++--
 project/MiMaFilters.scala                     |  8 ++
 .../neg-macros/newClassExtendsNoParents.check | 19 +++++
 .../newClassExtendsNoParents/Macro_1.scala    | 23 +++++
 .../newClassExtendsNoParents/Test_2.scala     |  1 +
 .../neg-macros/newClassExtendsOnlyTrait.check | 19 +++++
 .../newClassExtendsOnlyTrait/Macro_1.scala    | 31 +++++++
 .../newClassExtendsOnlyTrait/Test_2.scala     |  1 +
 tests/run-macros/newClass.check               |  4 +
 tests/run-macros/newClass/Macro_1.scala       | 21 +++++
 tests/run-macros/newClass/Test_2.scala        |  6 ++
 tests/run-macros/newClassExtends.check        |  4 +
 .../run-macros/newClassExtends/Macro_1.scala  | 31 +++++++
 tests/run-macros/newClassExtends/Test_2.scala |  8 ++
 .../newClassExtendsClassParams.check          |  4 +
 .../newClassExtendsClassParams/Macro_1.scala  | 27 ++++++
 .../newClassExtendsClassParams/Test_2.scala   |  8 ++
 tests/run-macros/newClassSelf.check           |  8 ++
 tests/run-macros/newClassSelf/Macro_1.scala   | 71 ++++++++++++++++
 tests/run-macros/newClassSelf/Test_2.scala    | 13 +++
 22 files changed, 408 insertions(+), 10 deletions(-)
 create mode 100644 tests/neg-macros/newClassExtendsNoParents.check
 create mode 100644 tests/neg-macros/newClassExtendsNoParents/Macro_1.scala
 create mode 100644 tests/neg-macros/newClassExtendsNoParents/Test_2.scala
 create mode 100644 tests/neg-macros/newClassExtendsOnlyTrait.check
 create mode 100644 tests/neg-macros/newClassExtendsOnlyTrait/Macro_1.scala
 create mode 100644 tests/neg-macros/newClassExtendsOnlyTrait/Test_2.scala
 create mode 100644 tests/run-macros/newClass.check
 create mode 100644 tests/run-macros/newClass/Macro_1.scala
 create mode 100644 tests/run-macros/newClass/Test_2.scala
 create mode 100644 tests/run-macros/newClassExtends.check
 create mode 100644 tests/run-macros/newClassExtends/Macro_1.scala
 create mode 100644 tests/run-macros/newClassExtends/Test_2.scala
 create mode 100644 tests/run-macros/newClassExtendsClassParams.check
 create mode 100644 tests/run-macros/newClassExtendsClassParams/Macro_1.scala
 create mode 100644 tests/run-macros/newClassExtendsClassParams/Test_2.scala
 create mode 100644 tests/run-macros/newClassSelf.check
 create mode 100644 tests/run-macros/newClassSelf/Macro_1.scala
 create mode 100644 tests/run-macros/newClassSelf/Test_2.scala

diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala
index b19c8cae3105..e61ba1d6201e 100644
--- a/compiler/src/dotty/tools/dotc/core/Symbols.scala
+++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala
@@ -577,7 +577,7 @@ object Symbols {
       def complete(denot: SymDenotation)(using Context): Unit = {
         val cls = denot.asClass.classSymbol
         val decls = newScope
-        denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls)
+        denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls, selfInfo)
       }
     }
     newClassSymbol(owner, name, flags, completer, privateWithin, coord, assocFile)
diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
index e97aa061c3f5..9fd7b3b7dce1 100644
--- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
+++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
@@ -227,6 +227,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
     end ClassDefTypeTest
 
     object ClassDef extends ClassDefModule:
+      def apply(cls: Symbol, parents: List[Tree], body: List[Statement]): ClassDef =
+        val untpdCtr = untpd.DefDef(nme.CONSTRUCTOR, Nil, tpd.TypeTree(dotc.core.Symbols.defn.UnitClass.typeRef), tpd.EmptyTree)
+        val ctr = ctx.typeAssigner.assignType(untpdCtr, cls.primaryConstructor)
+        tpd.ClassDefWithParents(cls.asClass, ctr, parents, body)
+
       def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree], selfOpt: Option[ValDef], body: List[Statement]): ClassDef = {
         val dotc.ast.Trees.TypeDef(_, originalImpl: tpd.Template) = original
         tpd.cpy.TypeDef(original)(name.toTypeName, tpd.cpy.Template(originalImpl)(constr, parents, derived = Nil, selfOpt.getOrElse(tpd.EmptyValDef), body))
@@ -259,6 +264,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
 
     object DefDef extends DefDefModule:
       def apply(symbol: Symbol, rhsFn: List[List[Tree]] => Option[Term]): DefDef =
+        assert(symbol.isTerm, s"expected a term symbol but received $symbol")
         withDefaultPos(tpd.DefDef(symbol.asTerm, prefss =>
           xCheckMacroedOwners(xCheckMacroValidExpr(rhsFn(prefss)), symbol).getOrElse(tpd.EmptyTree)
         ))
@@ -1803,7 +1809,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
         (x.prefix, x.name.toString)
     end TermRef
 
-    type TypeRef = dotc.core.Types.NamedType
+    type TypeRef = dotc.core.Types.TypeRef
 
     object TypeRefTypeTest extends TypeTest[TypeRepr, TypeRef]:
       def unapply(x: TypeRepr): Option[TypeRef & x.type] = x match
@@ -2453,6 +2459,20 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
       def requiredModule(path: String): Symbol = dotc.core.Symbols.requiredModule(path)
       def requiredMethod(path: String): Symbol = dotc.core.Symbols.requiredMethod(path)
       def classSymbol(fullName: String): Symbol = dotc.core.Symbols.requiredClass(fullName)
+
+      def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfInfo: Option[TypeRepr]): Symbol =
+        assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class")
+        val cls = dotc.core.Symbols.newNormalizedClassSymbol(
+          owner,
+          name.toTypeName,
+          dotc.core.Flags.EmptyFlags,
+          parents,
+          selfInfo.getOrElse(Types.NoType),
+          dotc.core.Symbols.NoSymbol)
+        cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil))
+        for sym <- decls(cls) do cls.enter(sym)
+        cls
+
       def newMethod(owner: Symbol, name: String, tpe: TypeRepr): Symbol =
         newMethod(owner, name, tpe, Flags.EmptyFlags, noSymbol)
       def newMethod(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
@@ -2621,6 +2641,8 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
         def companionClass: Symbol = self.denot.companionClass
         def companionModule: Symbol = self.denot.companionModule
         def children: List[Symbol] = self.denot.children
+        def typeRef: TypeRef = self.denot.typeRef
+        def termRef: TermRef = self.denot.termRef
 
         def show(using printer: Printer[Symbol]): String = printer.show(self)
 
diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala
index a9e2db108f40..45c7e5fc8c94 100644
--- a/library/src/scala/quoted/Quotes.scala
+++ b/library/src/scala/quoted/Quotes.scala
@@ -464,6 +464,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
 
     /** Methods of the module object `val ClassDef` */
     trait ClassDefModule { this: ClassDef.type =>
+      @experimental def apply(cls: Symbol, parents: List[Tree /* Term | TypeTree */], body: List[Statement]): ClassDef
       def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree /* Term | TypeTree */], selfOpt: Option[ValDef], body: List[Statement]): ClassDef
       def unapply(cdef: ClassDef): (String, DefDef, List[Tree /* Term | TypeTree */], Option[ValDef], List[Statement])
     }
@@ -3533,19 +3534,73 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
       /** The class Symbol of a global class definition */
       def classSymbol(fullName: String): Symbol
 
+      /** Generates a new class symbol for a class with a parameterless constructor.
+       *
+       *  Example usage:
+       *  ```scala
+       *  val name: String = ...
+       *  val parents = List(TypeTree.of[Object], TypeTree.of[Foo])
+       *  def decls(cls: Symbol): List[Symbol] =
+       *    List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit])))
+       *
+       *  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
+       *  val fooSym = cls.declaredMethod("foo").head
+       *
+       *  val fooDef = DefDef(fooSym, argss => Some('{println(s"Calling foo")}.asTerm))
+       *  val clsDef = ClassDef(cls, parents, body = List(fooDef))
+       *  val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Foo])
+       *
+       *  Block(List(clsDef), newCls).asExprOf[Foo]
+       *  ```
+       *  constructs the equivalent to
+       *   ```scala
+       *  '{
+       *    class `name`() extends Object with Foo {
+       *      def foo(): Unit = println("Calling foo")
+       *    }
+       *    new `name`(): Foo
+       *  }
+       *  ```
+       *
+       *  @param parent The owner of the class
+       *  @param name The name of the class
+       *  @param parents The parent classes of the class
+       *  @param decls The member declarations of the class provided the symbol of this class
+       *  @param self The self type of the class if it has one
+       *
+       *  This symbol starts without an accompanying definition.
+       *  It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
+       *  this symbol to the ClassDef constructor.
+       *
+       *  @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
+       *        direct or indirect children of the reflection context's owner.
+       */
+      @experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfInfo: Option[TypeRepr]): Symbol
+
       /** Generates a new method symbol with the given parent, name and type.
-      *
-      *  This symbol starts without an accompanying definition.
-      *  It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
-      *  this symbol to the DefDef constructor.
-      *
-      *  @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
-      *        direct or indirect children of the reflection context's owner.
-      */
+       *
+       *  To define a member method of a class, use the `newMethod` within the `decls` function of `newClass`.
+       *
+       *  @param parent The owner of the method
+       *  @param name The name of the method
+       *  @param tpe The type of the method (MethodType, PolyType, ByNameType)
+       *
+       *  This symbol starts without an accompanying definition.
+       *  It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
+       *  this symbol to the DefDef constructor.
+       *
+       *  @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
+       *        direct or indirect children of the reflection context's owner.
+       */
       def newMethod(parent: Symbol, name: String, tpe: TypeRepr): Symbol
 
       /** Works as the other newMethod, but with additional parameters.
       *
+      *  To define a member method of a class, use the `newMethod` within the `decls` function of `newClass`.
+      *
+      *  @param parent The owner of the method
+      *  @param name The name of the method
+      *  @param tpe The type of the method (MethodType, PolyType, ByNameType)
       *  @param flags extra flags to with which the symbol should be constructed
       *  @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
       */
@@ -3559,6 +3614,9 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
       *
       *  Note: Also see reflect.let
       *
+      *  @param parent The owner of the /var/lazy val
+      *  @param name The name of the val/var/lazy val
+      *  @param tpe The type of the val/var/lazy val
       *  @param flags extra flags to with which the symbol should be constructed
       *  @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
       *  @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
@@ -3572,7 +3630,10 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
       *  It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
       *  this symbol to the BindDef constructor.
       *
+      *  @param parent The owner of the binding
+      *  @param name The name of the binding
       *  @param flags extra flags to with which the symbol should be constructed
+      *  @param tpe The type of the binding
       *  @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
       *        direct or indirect children of the reflection context's owner.
       */
@@ -3807,6 +3868,14 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
 
         /** Case class or case object children of a sealed trait or cases of an `enum`. */
         def children: List[Symbol]
+
+        /** Type reference to the symbol */
+        @experimental
+        def typeRef: TypeRef
+
+        /** Term reference to the symbol */
+        @experimental
+        def termRef: TermRef
       end extension
     }
 
diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala
index 73b7e9ca674e..aff1a9baadb7 100644
--- a/project/MiMaFilters.scala
+++ b/project/MiMaFilters.scala
@@ -19,6 +19,14 @@ object MiMaFilters {
     ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#CompilationInfoModule.XmacroSettings"),
     ProblemFilters.exclude[DirectMissingMethodProblem]("scala.deriving.Mirror.fromProductTyped"),
     ProblemFilters.exclude[DirectMissingMethodProblem]("scala.deriving.Mirror.fromTuple"),
+    ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#ClassDefModule.apply"),
+    ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#ClassDefModule.apply"),
+    ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolModule.newClass"),
+    ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolModule.newClass"),
+    ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"),
+    ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"),
+    ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"),
+    ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"),
 
     // Private to the compiler - needed for forward binary compatibility
     ProblemFilters.exclude[MissingClassProblem]("scala.annotation.since")
diff --git a/tests/neg-macros/newClassExtendsNoParents.check b/tests/neg-macros/newClassExtendsNoParents.check
new file mode 100644
index 000000000000..c9637033d50e
--- /dev/null
+++ b/tests/neg-macros/newClassExtendsNoParents.check
@@ -0,0 +1,19 @@
+
+-- Error: tests/neg-macros/newClassExtendsNoParents/Test_2.scala:1:25 --------------------------------------------------
+1 |def test: Any = makeClass("foo") // error
+  |                ^^^^^^^^^^^^^^^^
+  |                Exception occurred while executing macro expansion.
+  |                java.lang.AssertionError: assertion failed: First parent must be a class
+  |                	at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
+  |                	at scala.quoted.runtime.impl.QuotesImpl$reflect$Symbol$.newClass(QuotesImpl.scala:2464)
+  |                	at scala.quoted.runtime.impl.QuotesImpl$reflect$Symbol$.newClass(QuotesImpl.scala:2463)
+  |                	at Macro_1$package$.makeClassExpr(Macro_1.scala:11)
+  |                	at Macro_1$package$.inline$makeClassExpr(Macro_1.scala:4)
+  |
+  |---------------------------------------------------------------------------------------------------------------------
+  |Inline stack trace
+  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+  |This location contains code that was inlined from Macro_1.scala:3
+3 |inline def makeClass(inline name: String): Any = ${ makeClassExpr('name) }
+  |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^
+   ---------------------------------------------------------------------------------------------------------------------
diff --git a/tests/neg-macros/newClassExtendsNoParents/Macro_1.scala b/tests/neg-macros/newClassExtendsNoParents/Macro_1.scala
new file mode 100644
index 000000000000..170e2f7d3f4b
--- /dev/null
+++ b/tests/neg-macros/newClassExtendsNoParents/Macro_1.scala
@@ -0,0 +1,23 @@
+import scala.quoted.*
+
+inline def makeClass(inline name: String): Any = ${ makeClassExpr('name) }
+private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Any] = {
+  import quotes.reflect.*
+
+  val name = nameExpr.valueOrAbort
+  val parents = List.empty[Tree] // BUG: first parent is not a class
+  def decls(cls: Symbol): List[Symbol] = Nil
+
+  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = Nil, decls, selfInfo = None)
+  val clsDef = ClassDef(cls, parents, body = List())
+  val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Object])
+
+  Block(List(clsDef), newCls).asExpr
+
+  // '{
+  //   class `name`() {
+  //     def foo(): Unit = println("Calling `name`.foo")
+  //   }
+  //   new `name`()
+  // }
+}
diff --git a/tests/neg-macros/newClassExtendsNoParents/Test_2.scala b/tests/neg-macros/newClassExtendsNoParents/Test_2.scala
new file mode 100644
index 000000000000..eada3ba7e67a
--- /dev/null
+++ b/tests/neg-macros/newClassExtendsNoParents/Test_2.scala
@@ -0,0 +1 @@
+def test: Any = makeClass("foo") // error
diff --git a/tests/neg-macros/newClassExtendsOnlyTrait.check b/tests/neg-macros/newClassExtendsOnlyTrait.check
new file mode 100644
index 000000000000..140da03c93a1
--- /dev/null
+++ b/tests/neg-macros/newClassExtendsOnlyTrait.check
@@ -0,0 +1,19 @@
+
+-- Error: tests/neg-macros/newClassExtendsOnlyTrait/Test_2.scala:1:25 --------------------------------------------------
+1 |def test: Foo = makeClass("foo") // error
+  |                ^^^^^^^^^^^^^^^^
+  |                Exception occurred while executing macro expansion.
+  |                java.lang.AssertionError: assertion failed: First parent must be a class
+  |                	at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
+  |                	at scala.quoted.runtime.impl.QuotesImpl$reflect$Symbol$.newClass(QuotesImpl.scala:2464)
+  |                	at scala.quoted.runtime.impl.QuotesImpl$reflect$Symbol$.newClass(QuotesImpl.scala:2463)
+  |                	at Macro_1$package$.makeClassExpr(Macro_1.scala:12)
+  |                	at Macro_1$package$.inline$makeClassExpr(Macro_1.scala:4)
+  |
+  |---------------------------------------------------------------------------------------------------------------------
+  |Inline stack trace
+  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+  |This location contains code that was inlined from Macro_1.scala:3
+3 |inline def makeClass(inline name: String): Foo = ${ makeClassExpr('name) }
+  |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^
+   ---------------------------------------------------------------------------------------------------------------------
diff --git a/tests/neg-macros/newClassExtendsOnlyTrait/Macro_1.scala b/tests/neg-macros/newClassExtendsOnlyTrait/Macro_1.scala
new file mode 100644
index 000000000000..8ff570b2d9e2
--- /dev/null
+++ b/tests/neg-macros/newClassExtendsOnlyTrait/Macro_1.scala
@@ -0,0 +1,31 @@
+import scala.quoted.*
+
+inline def makeClass(inline name: String): Foo = ${ makeClassExpr('name) }
+private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Foo] = {
+  import quotes.reflect.*
+
+  val name = nameExpr.valueOrAbort
+  val parents = List(TypeTree.of[Foo]) // BUG: first parent is not a class
+  def decls(cls: Symbol): List[Symbol] =
+    List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit])))
+
+  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
+  val fooSym = cls.declaredMethod("foo").head
+
+  val fooDef = DefDef(fooSym, argss => Some('{println(s"Calling ${$nameExpr}.foo")}.asTerm))
+  val clsDef = ClassDef(cls, parents, body = List(fooDef))
+  val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Foo])
+
+  Block(List(clsDef), newCls).asExprOf[Foo]
+
+  // '{
+  //   class `name`() extends Foo {
+  //     def foo(): Unit = println("Calling `name`.foo")
+  //   }
+  //   new `name`()
+  // }
+}
+
+trait Foo {
+  def foo(): Unit
+}
diff --git a/tests/neg-macros/newClassExtendsOnlyTrait/Test_2.scala b/tests/neg-macros/newClassExtendsOnlyTrait/Test_2.scala
new file mode 100644
index 000000000000..d8636c811214
--- /dev/null
+++ b/tests/neg-macros/newClassExtendsOnlyTrait/Test_2.scala
@@ -0,0 +1 @@
+def test: Foo = makeClass("foo") // error
diff --git a/tests/run-macros/newClass.check b/tests/run-macros/newClass.check
new file mode 100644
index 000000000000..581350beea39
--- /dev/null
+++ b/tests/run-macros/newClass.check
@@ -0,0 +1,4 @@
+Constructing foo
+class Test_2$package$foo$1
+Constructing bar
+class Test_2$package$bar$1
diff --git a/tests/run-macros/newClass/Macro_1.scala b/tests/run-macros/newClass/Macro_1.scala
new file mode 100644
index 000000000000..40ed128e43a0
--- /dev/null
+++ b/tests/run-macros/newClass/Macro_1.scala
@@ -0,0 +1,21 @@
+import scala.quoted.*
+
+inline def makeClass(inline name: String): Any = ${ makeClassExpr('name) }
+private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Any] = {
+  import quotes.reflect.*
+
+  val name = nameExpr.valueOrAbort
+  val parents = List(TypeTree.of[Object])
+  def decls(cls: Symbol): List[Symbol] = Nil
+
+  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
+
+  val clsDef = ClassDef(cls, parents, body = List('{println(s"Constructing ${$nameExpr}")}.asTerm))
+  val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Object])
+
+  Block(List(clsDef), newCls).asExpr
+  // '{
+  //   class `name`() { println("Constructing `name`") }
+  //   new `name`()
+  // }
+}
diff --git a/tests/run-macros/newClass/Test_2.scala b/tests/run-macros/newClass/Test_2.scala
new file mode 100644
index 000000000000..6868674547fe
--- /dev/null
+++ b/tests/run-macros/newClass/Test_2.scala
@@ -0,0 +1,6 @@
+@main def Test: Unit = {
+  val foo = makeClass("foo")
+  println(foo.getClass)
+  val bar = makeClass("bar")
+  println(bar.getClass)
+}
diff --git a/tests/run-macros/newClassExtends.check b/tests/run-macros/newClassExtends.check
new file mode 100644
index 000000000000..c8f9c6373f19
--- /dev/null
+++ b/tests/run-macros/newClassExtends.check
@@ -0,0 +1,4 @@
+Calling foo.foo
+class Test_2$package$foo$1
+Calling bar.foo
+class Test_2$package$bar$1
diff --git a/tests/run-macros/newClassExtends/Macro_1.scala b/tests/run-macros/newClassExtends/Macro_1.scala
new file mode 100644
index 000000000000..e88f63b52122
--- /dev/null
+++ b/tests/run-macros/newClassExtends/Macro_1.scala
@@ -0,0 +1,31 @@
+import scala.quoted.*
+
+inline def makeClass(inline name: String): Foo = ${ makeClassExpr('name) }
+private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Foo] = {
+  import quotes.reflect.*
+
+  val name = nameExpr.valueOrAbort
+  val parents = List(TypeTree.of[Object], TypeTree.of[Foo])
+  def decls(cls: Symbol): List[Symbol] =
+    List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit])))
+
+  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
+  val fooSym = cls.declaredMethod("foo").head
+
+  val fooDef = DefDef(fooSym, argss => Some('{println(s"Calling ${$nameExpr}.foo")}.asTerm))
+  val clsDef = ClassDef(cls, parents, body = List(fooDef))
+  val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Foo])
+
+  Block(List(clsDef), newCls).asExprOf[Foo]
+
+  // '{
+  //   class `name`() extends Object, Foo {
+  //     def foo(): Unit = println("Calling `name`.foo")
+  //   }
+  //   new `name`()
+  // }
+}
+
+trait Foo {
+  def foo(): Unit
+}
diff --git a/tests/run-macros/newClassExtends/Test_2.scala b/tests/run-macros/newClassExtends/Test_2.scala
new file mode 100644
index 000000000000..1db791749bab
--- /dev/null
+++ b/tests/run-macros/newClassExtends/Test_2.scala
@@ -0,0 +1,8 @@
+@main def Test: Unit = {
+  val foo: Foo = makeClass("foo")
+  foo.foo()
+  println(foo.getClass)
+  val bar: Foo = makeClass("bar")
+  bar.foo()
+  println(bar.getClass)
+}
diff --git a/tests/run-macros/newClassExtendsClassParams.check b/tests/run-macros/newClassExtendsClassParams.check
new file mode 100644
index 000000000000..38089ae4874a
--- /dev/null
+++ b/tests/run-macros/newClassExtendsClassParams.check
@@ -0,0 +1,4 @@
+Calling Foo.foo with i = 1
+class Test_2$package$foo$1
+Calling Foo.foo with i = 1
+class Test_2$package$bar$1
diff --git a/tests/run-macros/newClassExtendsClassParams/Macro_1.scala b/tests/run-macros/newClassExtendsClassParams/Macro_1.scala
new file mode 100644
index 000000000000..7444ce4f0c6d
--- /dev/null
+++ b/tests/run-macros/newClassExtendsClassParams/Macro_1.scala
@@ -0,0 +1,27 @@
+import scala.quoted.*
+
+inline def makeClass(inline name: String): Foo = ${ makeClassExpr('name) }
+private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Foo] = {
+  import quotes.reflect.*
+
+  val name = nameExpr.valueOrAbort
+  val parents = List('{ new Foo(1) }.asTerm)
+  def decls(cls: Symbol): List[Symbol] = Nil
+
+  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
+
+
+  val clsDef = ClassDef(cls, parents, body = Nil)
+  val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Foo])
+
+  Block(List(clsDef), newCls).asExprOf[Foo]
+
+  // '{
+  //   class `name`() extends Foo(3)
+  //   new `name`()
+  // }
+}
+
+class Foo(i: Int) {
+  def foo(): Unit = println(s"Calling Foo.foo with i = $i")
+}
diff --git a/tests/run-macros/newClassExtendsClassParams/Test_2.scala b/tests/run-macros/newClassExtendsClassParams/Test_2.scala
new file mode 100644
index 000000000000..1db791749bab
--- /dev/null
+++ b/tests/run-macros/newClassExtendsClassParams/Test_2.scala
@@ -0,0 +1,8 @@
+@main def Test: Unit = {
+  val foo: Foo = makeClass("foo")
+  foo.foo()
+  println(foo.getClass)
+  val bar: Foo = makeClass("bar")
+  bar.foo()
+  println(bar.getClass)
+}
diff --git a/tests/run-macros/newClassSelf.check b/tests/run-macros/newClassSelf.check
new file mode 100644
index 000000000000..f18a088495bc
--- /dev/null
+++ b/tests/run-macros/newClassSelf.check
@@ -0,0 +1,8 @@
+Calling Bar.bar
+Calling Foo.foo
+Calling Bar.bar
+class Test_2$package$A$1
+Calling Bar.bar
+Calling Foo.foo
+Calling Bar.bar
+class Test_2$package$B$1
diff --git a/tests/run-macros/newClassSelf/Macro_1.scala b/tests/run-macros/newClassSelf/Macro_1.scala
new file mode 100644
index 000000000000..0a38034809ff
--- /dev/null
+++ b/tests/run-macros/newClassSelf/Macro_1.scala
@@ -0,0 +1,71 @@
+import scala.quoted.*
+
+inline def makeClass(inline name: String): Bar = ${ makeClassExpr('name) }
+private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Bar] = {
+  import quotes.reflect.*
+  val name = nameExpr.valueOrAbort
+  val fooDef = makeFoo()
+  val fooBarDef = makeFooBar(name, fooDef.symbol)
+  val newCls = makeNewFooBar(fooBarDef.symbol)
+
+  Block(List(fooDef, fooBarDef), newCls).asExprOf[Bar]
+  // '{
+  //   class Foo { self: Bar =>
+  //      def foo(): Unit = bar()
+  //   }
+  //   class `name`() extends Foo with Bar
+  //   new `name`()
+  // }
+}
+
+/** Generate
+ *  ```
+ *  class Foo { self: Bar =>
+ *    def foo(): Unit = bar()
+ *  }
+ * ```
+ */
+def makeFoo(using Quotes)(): quotes.reflect.ClassDef = {
+  import quotes.reflect.*
+  val parents = List(TypeTree.of[Object])
+  def decls(cls: Symbol): List[Symbol] =
+    List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit])))
+
+  val cls = Symbol.newClass(Symbol.spliceOwner, "Foo", parents = parents.map(_.tpe), decls, selfInfo = Some(TypeRepr.of[Bar]))
+  val fooSym = cls.declaredMethod("foo").head
+  val barSym = Symbol.classSymbol("Bar").declaredMethod("bar").head
+
+  def fooRhs(args: List[List[Tree]]): Option[Term] =
+    val barCall = This(cls).select(barSym).appliedToNone.asExprOf[Unit]
+    Some('{ println("Calling Foo.foo"); $barCall }.asTerm)
+
+  val fooDef = DefDef(fooSym, fooRhs)
+  ClassDef(cls, parents, body = List(fooDef))
+}
+
+/** Generate
+ *  ```
+ *  class `name`() extends Foo with Bar
+ * ```
+ */
+def makeFooBar(using Quotes)(name: String, fooCls: quotes.reflect.Symbol): quotes.reflect.ClassDef = {
+  import quotes.reflect.*
+  val parents = List(Inferred(fooCls.typeRef), TypeTree.of[Bar])
+  def decls(cls: Symbol): List[Symbol] = Nil
+  val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
+  ClassDef(cls, parents, body = Nil)
+}
+
+/** Generate
+ *  ```
+ *  new `name`()
+ *  ```
+ */
+def makeNewFooBar(using Quotes)(fooBarCls: quotes.reflect.Symbol): quotes.reflect.Term = {
+  import quotes.reflect.*
+  Typed(Apply(Select(New(TypeIdent(fooBarCls)), fooBarCls.primaryConstructor), Nil), TypeTree.of[Bar])
+}
+
+trait Bar {
+  def bar(): Unit = println("Calling Bar.bar")
+}
diff --git a/tests/run-macros/newClassSelf/Test_2.scala b/tests/run-macros/newClassSelf/Test_2.scala
new file mode 100644
index 000000000000..0c6d522de58f
--- /dev/null
+++ b/tests/run-macros/newClassSelf/Test_2.scala
@@ -0,0 +1,13 @@
+@main def Test: Unit = {
+  val a: Bar = makeClass("A")
+  a.bar()
+  callFoo(a)
+  println(a.getClass)
+  val b: Bar = makeClass("B")
+  b.bar()
+  callFoo(b)
+  println(b.getClass)
+}
+
+def callFoo(x: Any): Unit =
+  x.getClass.getMethod("foo").invoke(x)