@@ -19,32 +19,51 @@ class NewTypeTreeTransformer extends UntypedTreeMap:
1919
2020 /** Transform a list of statements, expanding @newtype/@newsubtype classes. */
2121 private def transformStats (stats : List [Tree ])(using Context ): List [Tree ] =
22+ val statsList = stats.toIndexedSeq
23+ // Pre-scan: for each @newtype TypeDef, find a ModuleDef anywhere later in the same
24+ // statement list whose term-name matches. Macro paradise on Scala 2 finds companions via the
25+ // typer's symbol lookup, which works regardless of intervening `type`/`val` decls. We need to
26+ // emulate that here — using only the immediately-next stat is too narrow for codebases that
27+ // place companion objects after `type` aliases (a common idiom inside `package object`s).
28+ val consumedCompanion = scala.collection.mutable.Set .empty[Int ]
29+ val companionForNewtype = scala.collection.mutable.Map .empty[Int , Int ]
30+ for i <- statsList.indices do
31+ statsList(i) match
32+ case td : TypeDef if td.rhs.isInstanceOf [Template ] && findNewTypeAnnotation(td).isDefined =>
33+ val target = td.name.toTermName
34+ var j = i + 1
35+ var found = - 1
36+ while j < statsList.length && found < 0 do
37+ statsList(j) match
38+ case md : ModuleDef if md.name == target && ! consumedCompanion(j) =>
39+ found = j
40+ case _ =>
41+ j += 1
42+ if found >= 0 then
43+ companionForNewtype(i) = found
44+ consumedCompanion += found
45+ case _ =>
46+
2247 val result = ListBuffer .empty[Tree ]
2348 var i = 0
24- val statsList = stats.toIndexedSeq
2549 while i < statsList.length do
26- val stat = statsList(i)
27- stat match
28- case td : TypeDef if td.rhs.isInstanceOf [Template ] =>
29- findNewTypeAnnotation(td) match
30- case Some ((annotation, isSubtype)) =>
31- val params = AnnotationParams .extract(annotation, isSubtype)
32- // Look ahead for companion object
33- val companionOpt = if i + 1 < statsList.length then
34- statsList(i + 1 ) match
35- case md : ModuleDef if md.name.toTermName == td.name.toTermName => Some (md)
36- case _ => None
37- else None
38-
39- val expanded = expandNewType(td, companionOpt, params)
40- result ++= expanded
41-
42- if companionOpt.isDefined then i += 1 // skip companion, we merged it
43- case None =>
44- result += transform(stat)
45- case _ =>
46- result += transform(stat)
47- i += 1
50+ if consumedCompanion(i) then
51+ i += 1
52+ else
53+ statsList(i) match
54+ case td : TypeDef if td.rhs.isInstanceOf [Template ] =>
55+ findNewTypeAnnotation(td) match
56+ case Some ((annotation, isSubtype)) =>
57+ val params = AnnotationParams .extract(annotation, isSubtype)
58+ val companionOpt = companionForNewtype.get(i)
59+ .map(j => statsList(j).asInstanceOf [ModuleDef ])
60+ val expanded = expandNewType(td, companionOpt, params)
61+ result ++= expanded
62+ case None =>
63+ result += transform(statsList(i))
64+ case _ =>
65+ result += transform(statsList(i))
66+ i += 1
4867 result.toList
4968
5069 /** Find @newtype or @newsubtype annotation on a TypeDef. */
@@ -138,11 +157,6 @@ class NewTypeTreeTransformer extends UntypedTreeMap:
138157 | def derivingK[TC[_[_]]](implicit ev: TC[Repr]): TC[Type] = ev.asInstanceOf[TC[Type]] """ .stripMargin
139158 else " "
140159
141- // Merge with existing companion body
142- val existingCompanionCode = companionOpt.map { md =>
143- md.impl.body.map(plain(_)).mkString(" \n " , " \n " , " " )
144- }.getOrElse(" " )
145-
146160 // Coercible implicits: for parameterized types, use def with type params
147161 val C = " _root_.io.estatico.newtype.Coercible"
148162 val coercibleImplicits = if tparams.isEmpty then
@@ -182,15 +196,35 @@ class NewTypeTreeTransformer extends UntypedTreeMap:
182196 | $opsCode
183197 | $derivingCode
184198 | $derivingKCode
185- | $existingCompanionCode
186199 |} """ .stripMargin
187200
188201 if params.debug then
189202 println(s " [newtype-compat] Generated source for @ ${if params.isSubtype then " newsubtype" else " newtype" } $nameStr: " )
190203 source.linesIterator.foreach(line => println(s " [newtype-compat] $line" ))
191204
192- // Parse the generated source code into untyped trees
193- parseStats(source)
205+ // Parse the generated source code into untyped trees, then splice the companion's body
206+ // structurally (rather than via tree.show round-trip — which emits the `module` soft keyword
207+ // for nested objects and produces source that won't reparse).
208+ val stats = parseStats(source)
209+ companionOpt match
210+ case None => stats
211+ case Some (companion) =>
212+ val companionTermName = name.toTermName
213+ stats.map {
214+ case md : ModuleDef if md.name == companionTermName =>
215+ // Splice the companion's full Template into the synthesized one. We need to carry
216+ // over `parents` (e.g. `extends WithGenericSalesforceId[...]`), `derived` (any
217+ // `derives` clauses), and `self` from the original — the synthesized object only
218+ // provides default-empty values for those. Body members from both are concatenated.
219+ val mergedImpl = cpy.Template (md.impl)(
220+ parents = if companion.impl.parents.nonEmpty then companion.impl.parents else md.impl.parents,
221+ derived = if companion.impl.derived.nonEmpty then companion.impl.derived else md.impl.derived,
222+ self = if companion.impl.self != md.impl.self then companion.impl.self else md.impl.self,
223+ body = md.impl.body ++ companion.impl.body
224+ )
225+ untpd.ModuleDef (md.name, mergedImpl).withMods(md.mods).withSpan(md.span)
226+ case other => other
227+ }
194228
195229 private def buildOpsCodeFromMethods (
196230 nameStr : String , reprNameStr : String , reprTypeStr : String ,
@@ -285,6 +319,9 @@ class NewTypeTreeTransformer extends UntypedTreeMap:
285319 self = transformSub(md.impl.self),
286320 body = transformStats(md.impl.body)
287321 )
288- untpd.ModuleDef (md.name, newImpl).withSpan(md.span)
322+ // Preserve the original modifiers (incl. the package-object marker) — without `withMods`,
323+ // a `package object` would be reconstructed as a regular `object`, breaking sub-packages
324+ // and parent-package-object scope inheritance.
325+ untpd.ModuleDef (md.name, newImpl).withMods(md.mods).withSpan(md.span)
289326 case _ =>
290327 super .transform(tree)
0 commit comments