Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that Scala 2.13 can transform Scala 3 code and vice versa #647

Merged
merged 4 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,11 @@ val publishSettings = Seq(
val mimaSettings = Seq(
mimaPreviousArtifacts := {
val previousVersions = moduleName.value match {
case "chimney-macro-commons" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0")
case "chimney" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0")
case "chimney-cats" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0")
case "chimney-java-collections" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0")
case "chimney-protobufs" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0")
case "chimney-macro-commons" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0")
case "chimney" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0")
case "chimney-cats" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0")
case "chimney-java-collections" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0")
case "chimney-protobufs" => Set("1.0.0-RC1", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0")
case _ => Set()
}
previousVersions.map(organization.value %% moduleName.value % _)
Expand All @@ -287,6 +287,7 @@ val noPublishSettings =

val ciCommand = (platform: String, scalaSuffix: String) => {
val isJVM = platform == "JVM"
val isSandwichable = isJVM && scalaSuffix != "2_12"

val clean = Vector("clean")
def withCoverage(tasks: String*): Vector[String] =
Expand All @@ -298,6 +299,7 @@ val ciCommand = (platform: String, scalaSuffix: String) => {
"chimneyCats",
"chimneyProtobufs",
if (isJVM) "chimneyJavaCollections" else "",
if (isSandwichable) "chimneySandwichTests" else "",
"chimneyEngine"
)
if name.nonEmpty
Expand Down Expand Up @@ -345,6 +347,7 @@ lazy val root = project
.aggregate(chimneyJavaCollections.projectRefs *)
.aggregate(chimneyProtobufs.projectRefs *)
.aggregate(chimneyEngine.projectRefs *)
.aggregate(chimneySandwichTests.projectRefs *)
.settings(
moduleName := "chimney-build",
name := "chimney-build",
Expand Down Expand Up @@ -555,6 +558,48 @@ lazy val chimneyEngine = projectMatrix
.settings(dependencies *)
.dependsOn(chimney)

lazy val chimneySandwichTestCases213 = projectMatrix
.in(file("chimney-sandwich-test-cases-213"))
.someVariations(List(versions.scala213), List(VirtualAxis.jvm))()
.settings(settings *)
.settings(publishSettings *)
.settings(noPublishSettings *)
.settings(
moduleName := "chimney-sandwich-test-cases-213",
name := "chimney-sandwich-test-cases-213",
description := "Tests cases compiled with Scala 2.13 to test macros in 2.13x3 cross-compilation",
mimaFailOnNoPrevious := false // this module is not published
)

lazy val chimneySandwichTestCases3 = projectMatrix
.in(file("chimney-sandwich-test-cases-3"))
.someVariations(List(versions.scala3), List(VirtualAxis.jvm))()
.settings(settings *)
.settings(publishSettings *)
.settings(noPublishSettings *)
.settings(
moduleName := "chimney-sandwich-test-cases-3",
name := "chimney-sandwich-test-cases-3",
description := "Tests cases compiled with Scala 3 to test macros in 2.13x3 cross-compilation",
mimaFailOnNoPrevious := false // this module is not published
)

lazy val chimneySandwichTests = projectMatrix
.in(file("chimney-sandwich-tests"))
.someVariations(List(versions.scala213, versions.scala3), List(VirtualAxis.jvm))(only1VersionInIDE *)
.settings(settings *)
.settings(publishSettings *)
.settings(noPublishSettings *)
.settings(
moduleName := "chimney-sandwich-tests",
name := "chimney-sandwich-tests",
description := "Tests macros in 2.13x3 cross-compilation",
mimaFailOnNoPrevious := false // this module is not published
)
.dependsOn(chimney % s"$Test->$Test;$Compile->$Compile")
.dependsOn(chimneySandwichTestCases213 % s"$Test->$Test;$Compile->$Compile")
.dependsOn(chimneySandwichTestCases3 % s"$Test->$Test;$Compile->$Compile")

lazy val benchmarks = projectMatrix
.in(file("benchmarks"))
.someVariations(List(versions.scala213), List(VirtualAxis.jvm))(only1VersionInIDE *) // only makes sense for JVM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatfo

object platformSpecific {

/** Symbol for public primary constructor if it exists */
def publicPrimaryConstructor(tpe: c.Type): Option[Symbol] =
scala
.Option(tpe.typeSymbol)
.filter(_.isClass)
.map(_.asClass.primaryConstructor)
.filter(m => m.isPublic && m.isConstructor)

/** Finds all public constructors */
def publicConstructors(tpe: c.Type): List[Symbol] =
tpe.decls
.filter(m => m.isPublic && m.isConstructor)
.toList

/** Unambiguous constructor */
def publicPrimaryOrOnlyPublicConstructor(tpe: c.Type): Option[Symbol] =
publicPrimaryConstructor(tpe).orElse {
val candidates = publicConstructors(tpe)
if (candidates.size == 1) candidates.headOption else None
}

/** Nice alias for turning type representation with no type in its signature into Type[A] */
def fromUntyped[A](untyped: c.Type): Type[A] = c.WeakTypeTag(untyped)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform =>
def isPOJO[A](implicit A: Type[A]): Boolean = {
val sym = A.tpe.typeSymbol
!A.isPrimitive && !(A <:< Type[String]) && !sym.isJavaEnum && sym.isClass && !sym.isAbstract &&
sym.asClass.primaryConstructor != NoSymbol && sym.asClass.primaryConstructor.isPublic
publicPrimaryOrOnlyPublicConstructor(A.tpe).isDefined
}
def isCaseClass[A](implicit A: Type[A]): Boolean =
isPOJO[A] && A.tpe.typeSymbol.asClass.isCaseClass
Expand Down Expand Up @@ -116,15 +116,14 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform =>
} else if (isCaseObject[A] || isCaseVal[A]) {
Some(Product.Constructor(ListMap.empty, _ => c.Expr[A](q"${sym.asClass.module}")))
} else if (isPOJO[A]) {
val primaryConstructor =
Option(sym).filter(_.isClass).map(_.asClass.primaryConstructor).filter(_.isPublic).getOrElse {
// $COVERAGE-OFF$should never happen unless someone mess around with type-level representation
assertionFailed(s"Expected public constructor of ${Type.prettyPrint[A]}")
// $COVERAGE-ON$
}
val paramss = paramListsOf(A, primaryConstructor)
val unambiguousConstructor = publicPrimaryOrOnlyPublicConstructor(A).getOrElse {
// $COVERAGE-OFF$should never happen unless someone mess around with type-level representation
assertionFailed(s"Expected public constructor of ${Type.prettyPrint[A]}")
// $COVERAGE-ON$
}
val paramss = paramListsOf(A, unambiguousConstructor)
val paramNames = paramss.flatMap(_.map(param => param -> getDecodedName(param))).toMap
val paramTypes = paramsWithTypes(A, primaryConstructor)
val paramTypes = paramsWithTypes(A, unambiguousConstructor)
lazy val companion = companionSymbol[A]
val defaultValues = paramss.flatten.zipWithIndex.collect {
case (param, idx) if param.asTerm.isParamWithDefault =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ trait ValueClassesPlatform extends ValueClasses { this: DefinitionsPlatform =>
forceTypeSymbolInitialization[A]

val getterOpt: Option[Symbol] = A.decls.to(List).find(m => m.isPublic && m.isMethod && m.asMethod.isGetter)
val primaryConstructorOpt: Option[Symbol] = Option(A.typeSymbol)
.filter(_.isClass)
.map(_.asClass.primaryConstructor)
.find(m => m.isPublic && m.isConstructor && m.asMethod.paramLists.flatten.size == 1)
val argumentOpt: Option[Symbol] = primaryConstructorOpt.flatMap(_.asMethod.paramLists.flatten.headOption)
val unambiguousConstructorOpt: Option[Symbol] = publicPrimaryOrOnlyPublicConstructor(A)
.find(_.asMethod.paramLists.flatten.size == 1)
val argumentOpt: Option[Symbol] = unambiguousConstructorOpt.flatMap(_.asMethod.paramLists.flatten.headOption)

(getterOpt, primaryConstructorOpt, argumentOpt) match {
(getterOpt, unambiguousConstructorOpt, argumentOpt) match {
case (Some(getter), Some(pCtor), Some(argument))
if !Type[A].isPrimitive && getDecodedName(getter) == getDecodedName(argument) =>
val PCtor = pCtor.typeSignatureIn(A).asInstanceOf[MethodType]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatfo

object platformSpecific {

// to align API between Scala versions
extension (sym: Symbol) {
def isAbstract: Boolean = !sym.isNoSymbol && sym.flags.is(Flags.Abstract) || sym.flags.is(Flags.Trait)

def isPublic: Boolean = !sym.isNoSymbol &&
!(sym.flags.is(Flags.Private) || sym.flags.is(Flags.PrivateLocal) || sym.flags.is(Flags.Protected) ||
sym.privateWithin.isDefined || sym.protectedWithin.isDefined)
}

/** Symbol for public primary constructor if it exists */
def publicPrimaryConstructor(sym: Symbol): Option[Symbol] =
scala.Option(sym.primaryConstructor).filter(_.isPublic)

/** Finds all public constructors */
def publicConstructors(sym: Symbol): List[Symbol] =
sym.declarations.filter(_.isPublic).filter(_.isClassConstructor)

/** Unambiguous constructor */
def publicPrimaryOrOnlyPublicConstructor(sym: Symbol): Option[Symbol] =
publicPrimaryConstructor(sym).orElse {
val candidates = publicConstructors(sym)
if candidates.size == 1 then candidates.headOption else None
}

/** Nice alias for turning type representation with no type in its signature into Type[A] */
def fromUntyped[A](untyped: TypeRepr): Type[A] = untyped.asType.asInstanceOf[Type[A]]

// TODO: assumes each parameter list is made completely out of types OR completely out of values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,45 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform =>

// to align API between Scala versions
extension (sym: Symbol) {
def isAbstract: Boolean = !sym.isNoSymbol && sym.flags.is(Flags.Abstract) || sym.flags.is(Flags.Trait)
@deprecated("Moved to Type.platformSpecific", "1.6.0")
def isAbstract: Boolean = Type.platformSpecific.isPublic(sym)

def isPublic: Boolean = !sym.isNoSymbol &&
!(sym.flags.is(Flags.Private) || sym.flags.is(Flags.PrivateLocal) || sym.flags.is(Flags.Protected) ||
sym.privateWithin.isDefined || sym.protectedWithin.isDefined)
@deprecated("Moved to Type.platformSpecific", "1.6.0")
def isPublic: Boolean = Type.platformSpecific.isAbstract(sym)
}

def isParameterless(method: Symbol): Boolean =
method.paramSymss.filterNot(_.exists(_.isType)).flatten.isEmpty

def isDefaultConstructor(ctor: Symbol): Boolean =
ctor.isPublic && ctor.isClassConstructor && isParameterless(ctor)
Type.platformSpecific.isPublic(ctor) && ctor.isClassConstructor && isParameterless(ctor)

def isAccessor(accessor: Symbol): Boolean =
accessor.isPublic && accessor.isDefDef && isParameterless(accessor)
Type.platformSpecific.isPublic(accessor) && accessor.isDefDef && isParameterless(accessor)

// assuming isAccessor was tested earlier
def isJavaGetter(getter: Symbol): Boolean =
ProductTypes.BeanAware.isGetterName(getter.name)

def isJavaSetter(setter: Symbol): Boolean =
setter.isPublic && setter.isDefDef && setter.paramSymss.flatten.size == 1 && ProductTypes.BeanAware
.isSetterName(setter.name)
Type.platformSpecific.isPublic(setter) && setter.isDefDef && setter.paramSymss.flatten.size == 1 &&
ProductTypes.BeanAware.isSetterName(setter.name)

def isVar(setter: Symbol): Boolean =
setter.isPublic && (setter.isValDef || setter.isDefDef) && setter.flags.is(Flags.Mutable)
Type.platformSpecific.isPublic(setter) && (setter.isValDef || setter.isDefDef) && setter.flags.is(Flags.Mutable)

def isJavaSetterOrVar(setter: Symbol): Boolean =
isJavaSetter(setter) || isVar(setter)
}

import platformSpecific.*
import platformSpecific.{isAbstract as _, isPublic as _, *}
import Type.platformSpecific.*
import Type.Implicits.*

def isPOJO[A](implicit A: Type[A]): Boolean = {
val sym = TypeRepr.of(using A).typeSymbol
!A.isPrimitive && !(A <:< Type[String]) && sym.isClassDef && !sym.isAbstract && sym.primaryConstructor.isPublic
!A.isPrimitive && !(A <:< Type[String]) && sym.isClassDef && !sym.isAbstract &&
publicPrimaryOrOnlyPublicConstructor(sym).isDefined
}
def isCaseClass[A](implicit A: Type[A]): Boolean = {
val sym = TypeRepr.of(using A).typeSymbol
Expand Down Expand Up @@ -145,15 +146,15 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform =>
val A = TypeRepr.of[A]
val sym = A.typeSymbol

val primaryConstructor =
Option(sym.primaryConstructor).filter(_.isPublic).getOrElse {
val unambiguousConstructor =
publicPrimaryOrOnlyPublicConstructor(sym).getOrElse {
// $COVERAGE-OFF$should never happen unless we messed up
assertionFailed(s"Expected public constructor of ${Type.prettyPrint[A]}")
// $COVERAGE-ON$
}
val paramss = paramListsOf(A, primaryConstructor)
val paramss = paramListsOf(A, unambiguousConstructor)
val paramNames = paramss.flatMap(_.map(param => param -> param.name)).toMap
val paramTypes = paramsWithTypes(A, primaryConstructor, isConstructor = true)
val paramTypes = paramsWithTypes(A, unambiguousConstructor, isConstructor = true)
val defaultValues = paramss.flatten.zipWithIndex.collect {
case (param, idx) if param.flags.is(Flags.HasDefault) =>
val mod = sym.companionModule
Expand Down Expand Up @@ -222,7 +223,7 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform =>

def newExpr = {
// new A
val select = New(TypeTree.of[A]).select(primaryConstructor)
val select = New(TypeTree.of[A]).select(unambiguousConstructor)
// new A[B1, B2, ...] vs new A
val tree = if A.typeArgs.nonEmpty then select.appliedToTypes(A.typeArgs) else select
// new A... or new A() or new A(b1, b2), ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ trait ValueClassesPlatform extends ValueClasses { this: DefinitionsPlatform =>
val A: TypeRepr = TypeRepr.of[A]
val sym: Symbol = A.typeSymbol

val getterOpt: Option[Symbol] = sym.declarations.filter(isPublic).headOption
val primaryConstructorOpt: Option[Symbol] =
Option(sym.primaryConstructor).filterNot(_.isNoSymbol).filter(_.isClassConstructor).filter(isPublic)
val argumentOpt: Option[Symbol] = primaryConstructorOpt.flatMap { primaryConstructor =>
paramListsOf(A, primaryConstructor).flatten match {
val getterOpt: Option[Symbol] = sym.declarations.filter(_.isPublic).headOption
val unambiguousConstructorOpt: Option[Symbol] = publicPrimaryOrOnlyPublicConstructor(sym)
val argumentOpt: Option[Symbol] = unambiguousConstructorOpt.flatMap { unambiguousConstructor =>
paramListsOf(A, unambiguousConstructor).flatten match {
case argument :: Nil => Some(argument)
case _ => None
}
}

(getterOpt, primaryConstructorOpt, argumentOpt) match {
case (Some(getter), Some(primaryConstructor), Some(argument))
(getterOpt, unambiguousConstructorOpt, argumentOpt) match {
case (Some(getter), Some(unambiguousConstructor), Some(argument))
if !Type[A].isPrimitive && getter.name == argument.name =>
val Argument = fromUntyped[Any](paramsWithTypes(A, primaryConstructor, isConstructor = true)(argument.name))
val Argument =
fromUntyped[Any](paramsWithTypes(A, unambiguousConstructor, isConstructor = true)(argument.name))
val inner = returnTypeOf[Any](A, getter).as_??
import inner.Underlying as Inner
assert(
Expand All @@ -41,7 +41,7 @@ trait ValueClassesPlatform extends ValueClasses { this: DefinitionsPlatform =>
fieldName = getter.name,
unwrap = (expr: Expr[A]) => expr.asTerm.select(getter).appliedToArgss(Nil).asExprOf[Inner],
wrap = (expr: Expr[Inner]) => {
val select = New(TypeTree.of[A]).select(primaryConstructor)
val select = New(TypeTree.of[A]).select(unambiguousConstructor)
val tree = if A.typeArgs.nonEmpty then select.appliedToTypes(A.typeArgs) else select
tree.appliedToArgss(List(List(expr.asTerm))).asExprOf[A]
}
Expand All @@ -51,9 +51,5 @@ trait ValueClassesPlatform extends ValueClasses { this: DefinitionsPlatform =>
case _ => None
}
}

private def isPublic(sym: Symbol): Boolean =
!sym.isNoSymbol &&
(!(sym.flags.is(Flags.Private) || sym.flags.is(Flags.PrivateLocal) || sym.flags.is(Flags.Protected)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.scalaland.chimney.scala213

import scala.beans.BeanProperty

object BeanProperties {
final case class Foo private (
@BeanProperty var a: Int,
@BeanProperty var b: String,
@BeanProperty var c: Double,
@BeanProperty var d: Boolean
) { def this() = this(0, "", 0.0, false) }
final case class Bar private (
@BeanProperty var a: Int,
@BeanProperty var b: String,
@BeanProperty var c: Double
) { def this() = this(0, "", 0.0) }
final case class Baz private (
@BeanProperty var a: Int,
@BeanProperty var b: String,
@BeanProperty var c0: Double,
@BeanProperty var d: Int
) { def this() = this(0, "", 0.0, 0) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.scalaland.chimney.scala213

object Defaults {
final case class Foo(a: Int = 0, b: String = "", c: Double = 0.0, d: Boolean = false)
final case class Bar(a: Int = 0, b: String = "", c: Double = 0.0)
final case class Baz(a: Int = 0, b: String = "", c0: Double = 0.0, d: Int = 0)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.scalaland.chimney.scala213

object Monomorphic {
final case class Foo(a: Int, b: String, c: Double, d: Boolean)
final case class Bar(a: Int, b: String, c: Double)
final case class Baz(a: Int, b: String, c0: Double, d: Int)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.scalaland.chimney.scala213

object Polymorphic {
final case class Foo[A, B](a: A, b: B, c: Double, d: Boolean)
final case class Bar[A, B](a: A, b: B, c: Double)
final case class Baz[A, B](a: A, b: B, c0: Double, d: Int)
}
Loading
Loading