Skip to content

Commit 0cc17c0

Browse files
Support @newtype in package objects with cross-file usage
Rewrite package objects to nested PackageDefs during the plugin phase, avoiding Scala 3's "Trying to define package with same name as class" conflict when other files declare the same package. Also add Scala 3.8.3 to the supported versions list.
1 parent d40873f commit 0cc17c0

5 files changed

Lines changed: 77 additions & 2 deletions

File tree

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ val scala3Versions = Seq(
1313
"3.5.0", "3.5.1", "3.5.2",
1414
"3.6.2", "3.6.3", "3.6.4",
1515
"3.7.0", "3.7.1", "3.7.2", "3.7.3", "3.7.4",
16-
"3.8.0", "3.8.1", "3.8.2"
16+
"3.8.0", "3.8.1", "3.8.2", "3.8.3"
1717
)
1818

1919
val scala3Latest = scala3Versions.last

plugin/src/main/scala/io/estatico/newtype/compat/NewTypeTreeTransformer.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,16 @@ class NewTypeTreeTransformer extends UntypedTreeMap:
264264

265265
override def transform(tree: Tree)(using Context): Tree = tree match
266266
case PackageDef(pid, stats) =>
267-
cpy.PackageDef(tree)(transform(pid).asInstanceOf[RefTree], transformStats(stats))
267+
val transformedPid = transform(pid).asInstanceOf[RefTree]
268+
stats.partition { case md: ModuleDef => md.mods.is(Package); case _ => false } match
269+
case (pkgObj :: Nil, otherStats) =>
270+
val md = pkgObj.asInstanceOf[ModuleDef]
271+
val innerBody = transformStats(md.impl.body)
272+
val allInnerStats = otherStats.map(transform) ++ innerBody
273+
val newPid = Select(transformedPid, md.name)
274+
untpd.PackageDef(newPid, allInnerStats)
275+
case _ =>
276+
cpy.PackageDef(tree)(transformedPid, transformStats(stats))
268277
case Block(stats, expr) =>
269278
cpy.Block(tree)(transformStats(stats), transform(expr))
270279
case td @ TypeDef(name, rhs: Template) if !findNewTypeAnnotation(td).isDefined =>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.estatico.newtype.pkgobject
2+
3+
import io.estatico.newtype.ops._
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
import java.util.UUID
7+
8+
class PackageObjectTest extends AnyFlatSpec with Matchers {
9+
10+
behavior of "@newtype in package object"
11+
12+
it should "create a newtype defined in a package object" in {
13+
val uuid = UUID.randomUUID()
14+
val x: ClayTableId = ClayTableId(uuid)
15+
x.value shouldBe uuid
16+
x.coerce[UUID] shouldBe uuid
17+
}
18+
19+
it should "support wrapping via coerce" in {
20+
val uuid = UUID.randomUUID()
21+
val x = uuid.coerce[ClayTableId]
22+
x.value shouldBe uuid
23+
}
24+
25+
it should "work with multiple newtypes in the same package object" in {
26+
val x: ClayFieldName = ClayFieldName("test")
27+
x.value shouldBe "test"
28+
}
29+
30+
it should "make types accessible from other files in the same package" in {
31+
val uuid = UUID.randomUUID()
32+
val u = UsesPackageObjectTypes(ClayTableId(uuid), ClayFieldName("field"))
33+
u.tableId.value shouldBe uuid
34+
u.fieldName.value shouldBe "field"
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.estatico.newtype.pkgobject
2+
3+
import java.util.UUID
4+
5+
final case class UsesPackageObjectTypes(
6+
tableId: ClayTableId,
7+
fieldName: ClayFieldName
8+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.estatico.newtype
2+
3+
import cats.{Hash, Order, Show}
4+
import io.estatico.newtype.macros.newtype
5+
import java.util.UUID
6+
7+
package object pkgobject {
8+
9+
@newtype final case class ClayTableId(value: UUID)
10+
object ClayTableId {
11+
implicit lazy val showForClayTableId: Show[ClayTableId] = Show.show(_.value.toString())
12+
implicit lazy val orderForClayTableId: Order[ClayTableId] = Order.by(_.value)
13+
implicit lazy val hashForClayTableId: Hash[ClayTableId] = Hash.by(_.value)
14+
}
15+
16+
@newtype final case class ClayFieldName(value: String)
17+
object ClayFieldName {
18+
implicit lazy val showForClayFieldName: Show[ClayFieldName] = Show.show(_.value)
19+
implicit lazy val orderForClayFieldName: Order[ClayFieldName] = Order.by(_.value)
20+
implicit lazy val hashForClayFieldName: Hash[ClayFieldName] = Hash.by(_.value)
21+
}
22+
}

0 commit comments

Comments
 (0)