Skip to content

Commit

Permalink
Merge pull request #27 from kubukoz/feature/classes-in-objects
Browse files Browse the repository at this point in the history
Add support for classes nested in objects
  • Loading branch information
mrobakowski authored May 11, 2021
2 parents e0a6a6b + 25a6c43 commit 25a7182
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 18 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ The plugin is currently published for the following Scala versions:

1. Only case classes located directly in `package`s or `object`s are changed. Nested classes and classes local to functions are currently ignored.
2. Only the fields in the first parameter list are shown.
3. If the class already overrides `toString` *directly*, it's not replaced.
3. If the class is nested in an object (but not a package object), the object's name and a dot are prepended.
4. If the class already overrides `toString` *directly*, it's not replaced.

## Roadmap

- Ignore classes that inherit `toString` from a type that isn't `Object`
- Add a way of overriding default behavior (blacklisting/whitelisting certain classes) - probably via an annotation in an extra dependency
- Extend functionality to support ADTs - for example, `case object B extends A` inside `object A` could be shown as `A.B`
- Potentially ignore value classes

If you have ideas for improving the plugin, feel free to create an issue and I'll consider making it happen :)
Expand Down
4 changes: 2 additions & 2 deletions plugin/src/main/resources/scalac-plugin.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<plugin>
<name>better-tostring</name>
<classname>com.kubukoz.BetterToStringPlugin</classname>
<name>better-tostring</name>
<classname>com.kubukoz.BetterToStringPlugin</classname>
</plugin>
11 changes: 6 additions & 5 deletions plugin/src/main/scala-2/BetterToStringPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,19 @@ final class BetterToStringPluginComponent(val global: Global) extends PluginComp
private val impl: BetterToStringImpl[Scala2CompilerApi[global.type]] =
BetterToStringImpl.instance(Scala2CompilerApi.instance(global))

private def modifyClasses(tree: Tree): Tree =
private def modifyClasses(tree: Tree, enclosingObject: Option[ModuleDef]): Tree =
tree match {
case p: PackageDef => p.copy(stats = p.stats.map(modifyClasses))
case p: PackageDef => p.copy(stats = p.stats.map(modifyClasses(_, None)))
case m: ModuleDef =>
m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses)))
m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, Some(m)))))
case clazz: ClassDef =>
impl.transformClass(
clazz,
// If it was nested, we wouldn't be in this branch.
// Scala 2.x compiler API limitation (classes can't tell what the owner is).
// This should be more optimal as we don't traverse every template, but it hasn't been benchmarked.
isNested = false
isNested = false,
enclosingObject
)
case other => other
}
Expand All @@ -45,7 +46,7 @@ final class BetterToStringPluginComponent(val global: Global) extends PluginComp

override def apply(unit: CompilationUnit): Unit =
new Transformer {
override def transform(tree: Tree): Tree = modifyClasses(tree)
override def transform(tree: Tree): Tree = modifyClasses(tree, None)
}.transformUnit(unit)

}
Expand Down
7 changes: 7 additions & 0 deletions plugin/src/main/scala-2/Scala2CompilerApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ trait Scala2CompilerApi[G <: Global] extends CompilerApi {
type Param = ValDef
type ParamName = TermName
type Method = DefDef
type EnclosingObject = ModuleDef
}

object Scala2CompilerApi {
Expand All @@ -25,6 +26,12 @@ object Scala2CompilerApi {
}

def className(clazz: Clazz): String = clazz.name.toString

def isPackageOrPackageObject(enclosingObject: EnclosingObject): Boolean =
// couldn't find any nice api for this. `m.symbol.isPackageObject` does not work after the parser compiler phase (needs to run later).
enclosingObject.symbol.isInstanceOf[NoSymbol] && enclosingObject.name.toString == "package"

def enclosingObjectName(enclosingObject: EnclosingObject): String = enclosingObject.name.toString
def literalConstant(value: String): Tree = Literal(Constant(value))
def paramName(param: Param): ParamName = param.name
def selectInThis(clazz: Clazz, name: ParamName): Tree = q"this.$name"
Expand Down
13 changes: 11 additions & 2 deletions plugin/src/main/scala-3/BetterToStringPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package com.kubukoz
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Flags.Module
import dotty.tools.dotc.core.Flags.Package
import dotty.tools.dotc.core.Symbols
import dotty.tools.dotc.plugins.PluginPhase
import dotty.tools.dotc.plugins.StandardPlugin
import dotty.tools.dotc.typer.FrontEnd
import tpd._

import scala.annotation.tailrec

final class BetterToStringPlugin extends StandardPlugin:
override val name: String = "better-tostring"
override val description: String = "scala compiler plugin for better default toString implementations"
Expand All @@ -21,9 +25,14 @@ final class BetterToStringPluginPhase extends PluginPhase:
override def transformTemplate(t: Template)(using ctx: Context): Tree =
val clazz = ctx.owner.asClass

val isNested = !(ctx.owner.owner.isPackageObject || ctx.owner.owner.is(Module))
val ownerOwner = ctx.owner.owner
val isNested = ownerOwner.ownersIterator.exists(!_.is(Module))

val enclosingObject =
if (ownerOwner.is(Module)) then Some(ownerOwner)
else None

BetterToStringImpl
.instance(Scala3CompilerApi.instance)
.transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested)
.transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, enclosingObject)
.t
9 changes: 9 additions & 0 deletions plugin/src/main/scala-3/Scala3CompilerApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dotty.tools.dotc.core.Symbols
import dotty.tools.dotc.core.Flags.CaseAccessor
import dotty.tools.dotc.core.Flags.CaseClass
import dotty.tools.dotc.core.Flags.Override
import dotty.tools.dotc.core.Flags.Package
import dotty.tools.dotc.core.Types
import dotty.tools.dotc.core.Names
import dotty.tools.dotc.core.Constants.Constant
Expand All @@ -20,6 +21,7 @@ trait Scala3CompilerApi extends CompilerApi:
type Param = ValDef
type ParamName = Names.TermName
type Method = DefDef
type EnclosingObject = Symbols.Symbol

object Scala3CompilerApi:
final case class ClassContext(t: Template, clazz: ClassSymbol):
Expand All @@ -33,6 +35,13 @@ object Scala3CompilerApi:
}

def className(clazz: Clazz): String = clazz.clazz.name.toString

def isPackageOrPackageObject(enclosingObject: EnclosingObject): Boolean =
enclosingObject.is(Package) || enclosingObject.isPackageObject

def enclosingObjectName(enclosingObject: EnclosingObject): String =
enclosingObject.effectiveName.toString

def literalConstant(value: String): Tree = Literal(Constant(value))
def paramName(param: Param): ParamName = param.name
def selectInThis(clazz: Clazz, name: ParamName): Tree = This(clazz.clazz).select(name)
Expand Down
20 changes: 13 additions & 7 deletions plugin/src/main/scala/BetterToStringImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ trait CompilerApi {
type Param
type ParamName
type Method
type EnclosingObject

def className(clazz: Clazz): String
def isPackageOrPackageObject(enclosingObject: EnclosingObject): Boolean
def enclosingObjectName(enclosingObject: EnclosingObject): String
def params(clazz: Clazz): List[Param]
def literalConstant(value: String): Tree

Expand All @@ -28,7 +31,8 @@ trait BetterToStringImpl[+C <: CompilerApi] {

def transformClass(
clazz: compilerApi.Clazz,
isNested: Boolean
isNested: Boolean,
enclosingObject: Option[compilerApi.EnclosingObject]
): compilerApi.Clazz

}
Expand All @@ -45,22 +49,24 @@ object BetterToStringImpl {

def transformClass(
clazz: Clazz,
isNested: Boolean
isNested: Boolean,
enclosingObject: Option[EnclosingObject]
): Clazz = {
val hasToString: Boolean = methodNames(clazz).contains("toString")

val shouldModify =
isCaseClass(clazz) && !isNested && !hasToString

if (shouldModify) overrideToString(clazz)
if (shouldModify) overrideToString(clazz, enclosingObject)
else clazz
}

private def overrideToString(clazz: Clazz): Clazz =
addMethod(clazz, createToString(clazz, toStringImpl(clazz)))
private def overrideToString(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Clazz =
addMethod(clazz, createToString(clazz, toStringImpl(clazz, enclosingObject)))

private def toStringImpl(clazz: Clazz): Tree = {
private def toStringImpl(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Tree = {
val className = api.className(clazz)
val parentPrefix = enclosingObject.filterNot(api.isPackageOrPackageObject).fold("")(api.enclosingObjectName(_) ++ ".")

val paramListParts: List[Tree] = params(clazz).zipWithIndex.flatMap { case (v, index) =>
val commaPrefix = if (index > 0) ", " else ""
Expand All @@ -75,7 +81,7 @@ object BetterToStringImpl {

val parts =
List(
List(literalConstant(className ++ "(")),
List(literalConstant(parentPrefix ++ className ++ "(")),
paramListParts,
List(literalConstant(")"))
).flatten
Expand Down
28 changes: 28 additions & 0 deletions tests/src/test/scala/Demo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,30 @@ class Tests extends AnyWordSpec with Matchers {
}
}

"Class nested in an object" should {
"include enclosing object's name" in {
ObjectNestedParent.ObjectNestedClass("Joe").toString shouldBe "ObjectNestedParent.ObjectNestedClass(name = Joe)"
}
}

"Class nested in a package object" should {
"not include package's name" in {
pack.InPackageObject("Joe").toString shouldBe "InPackageObject(name = Joe)"
}
}

"Class nested in another class" should {
"stringify normally" in {
new NestedParent().NestedChild("a").toString shouldBe "NestedChild(a)"
}
}

"Class nested in an object itself nested in a class" should {
"stringify normally" in {
new DeeplyNestedInClassGrandparent().DeeplyNestedInClassParent.DeeplyNestedInClassClass("a").toString shouldBe "DeeplyNestedInClassClass(a)"
}
}

"Method-local class" should {
"stringify normally" in {
MethodLocalWrapper.methodLocalClassStringify shouldBe "LocalClass(a)"
Expand All @@ -63,6 +81,16 @@ final class NestedParent() {
case class NestedChild(name: String)
}

object ObjectNestedParent {
case class ObjectNestedClass(name: String)
}

final class DeeplyNestedInClassGrandparent {
object DeeplyNestedInClassParent {
case class DeeplyNestedInClassClass(name: String)
}
}

object MethodLocalWrapper {

def methodLocalClassStringify: String = {
Expand Down
3 changes: 3 additions & 0 deletions tests/src/test/scala/pack/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package object pack {
case class InPackageObject(name: String)
}

0 comments on commit 25a7182

Please sign in to comment.