From d198f522fac3e3a86187cafbd5e66b40f0eac660 Mon Sep 17 00:00:00 2001 From: mrobakowski Date: Tue, 20 Apr 2021 21:40:51 +0200 Subject: [PATCH 1/7] Add support for Nested.Classes(foo = 42) --- .../main/scala-2/BetterToStringPlugin.scala | 14 +++++++++----- .../src/main/scala-2/Scala2CompilerApi.scala | 2 ++ .../main/scala-3/BetterToStringPlugin.scala | 10 ++++++++-- .../src/main/scala-3/Scala3CompilerApi.scala | 2 ++ .../src/main/scala/BetterToStringImpl.scala | 19 ++++++++++++------- tests/src/test/scala/Demo.scala | 16 ++++++++++++++++ tests/src/test/scala/pack/package.scala | 3 +++ 7 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 tests/src/test/scala/pack/package.scala diff --git a/plugin/src/main/scala-2/BetterToStringPlugin.scala b/plugin/src/main/scala-2/BetterToStringPlugin.scala index 2846bcc..3588ede 100644 --- a/plugin/src/main/scala-2/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-2/BetterToStringPlugin.scala @@ -25,18 +25,22 @@ 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, parent: 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))) + // couldn't find any nice api for this. `m.symbol.isPackageObject` does not work + val isPackageObject = m.symbol.isInstanceOf[NoSymbol] && m.name.toString == "package" + val parent = if (!isPackageObject) Some(m) else None + m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, parent)))) 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, + parent ) case other => other } @@ -45,7 +49,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) } diff --git a/plugin/src/main/scala-2/Scala2CompilerApi.scala b/plugin/src/main/scala-2/Scala2CompilerApi.scala index 5af5301..d82cbb7 100644 --- a/plugin/src/main/scala-2/Scala2CompilerApi.scala +++ b/plugin/src/main/scala-2/Scala2CompilerApi.scala @@ -11,6 +11,7 @@ trait Scala2CompilerApi[G <: Global] extends CompilerApi { type Param = ValDef type ParamName = TermName type Method = DefDef + type ClazzParent = ModuleDef } object Scala2CompilerApi { @@ -25,6 +26,7 @@ object Scala2CompilerApi { } def className(clazz: Clazz): String = clazz.name.toString + def parentName(parent: ClazzParent) = parent.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" diff --git a/plugin/src/main/scala-3/BetterToStringPlugin.scala b/plugin/src/main/scala-3/BetterToStringPlugin.scala index 36440d2..726e730 100644 --- a/plugin/src/main/scala-3/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-3/BetterToStringPlugin.scala @@ -3,6 +3,7 @@ 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.plugins.PluginPhase import dotty.tools.dotc.plugins.StandardPlugin import dotty.tools.dotc.typer.FrontEnd @@ -21,9 +22,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.isPackageObject || ownerOwner.is(Module)) + val parent = if (ownerOwner.is(Module) && !ownerOwner.is(Package) && !ownerOwner.isPackageObject) + Some(ctx.owner.owner) + else + None BetterToStringImpl .instance(Scala3CompilerApi.instance) - .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested) + .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, parent) .t diff --git a/plugin/src/main/scala-3/Scala3CompilerApi.scala b/plugin/src/main/scala-3/Scala3CompilerApi.scala index 368c78a..fccdc0a 100644 --- a/plugin/src/main/scala-3/Scala3CompilerApi.scala +++ b/plugin/src/main/scala-3/Scala3CompilerApi.scala @@ -20,6 +20,7 @@ trait Scala3CompilerApi extends CompilerApi: type Param = ValDef type ParamName = Names.TermName type Method = DefDef + type ClazzParent = Symbols.Symbol object Scala3CompilerApi: final case class ClassContext(t: Template, clazz: ClassSymbol): @@ -32,6 +33,7 @@ object Scala3CompilerApi: } def className(clazz: Clazz): String = clazz.clazz.name.toString + def parentName(parent: ClazzParent): String = parent.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) diff --git a/plugin/src/main/scala/BetterToStringImpl.scala b/plugin/src/main/scala/BetterToStringImpl.scala index 5ec8e7c..cef430c 100644 --- a/plugin/src/main/scala/BetterToStringImpl.scala +++ b/plugin/src/main/scala/BetterToStringImpl.scala @@ -8,8 +8,10 @@ trait CompilerApi { type Param type ParamName type Method + type ClazzParent def className(clazz: Clazz): String + def parentName(parent: ClazzParent): String def params(clazz: Clazz): List[Param] def literalConstant(value: String): Tree @@ -28,7 +30,8 @@ trait BetterToStringImpl[+C <: CompilerApi] { def transformClass( clazz: compilerApi.Clazz, - isNested: Boolean + isNested: Boolean, + parent: Option[compilerApi.ClazzParent] ): compilerApi.Clazz } @@ -45,22 +48,24 @@ object BetterToStringImpl { def transformClass( clazz: Clazz, - isNested: Boolean + isNested: Boolean, + parent: Option[ClazzParent] ): Clazz = { val hasToString: Boolean = methodNames(clazz).contains("toString") val shouldModify = isCaseClass(clazz) && !isNested && !hasToString - if (shouldModify) overrideToString(clazz) + if (shouldModify) overrideToString(clazz, parent) else clazz } - private def overrideToString(clazz: Clazz): Clazz = - addMethod(clazz, createToString(clazz, toStringImpl(clazz))) + private def overrideToString(clazz: Clazz, parent: Option[ClazzParent]): Clazz = + addMethod(clazz, createToString(clazz, toStringImpl(clazz, parent))) - private def toStringImpl(clazz: Clazz): Tree = { + private def toStringImpl(clazz: Clazz, parent: Option[ClazzParent]): Tree = { val className = api.className(clazz) + val parentPrefix = parent.map(p => api.parentName(p) ++ ".").getOrElse("") val paramListParts: List[Tree] = params(clazz).zipWithIndex.flatMap { case (v, index) => val commaPrefix = if (index > 0) ", " else "" @@ -75,7 +80,7 @@ object BetterToStringImpl { val parts = List( - List(literalConstant(className ++ "(")), + List(literalConstant(parentPrefix ++ className ++ "(")), paramListParts, List(literalConstant(")")) ).flatten diff --git a/tests/src/test/scala/Demo.scala b/tests/src/test/scala/Demo.scala index 385f792..9565c13 100644 --- a/tests/src/test/scala/Demo.scala +++ b/tests/src/test/scala/Demo.scala @@ -35,6 +35,18 @@ class Tests extends AnyWordSpec with Matchers { } } + "Class nested in an object" should { + "include parent's name" in { + ObjectNestedParent.ObjectNestedClass("Joe").toString shouldBe "ObjectNestedParent.ObjectNestedClass(name = Joe)" + } + } + + "Class nested in a package object" should { + "not include parent'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)" @@ -63,6 +75,10 @@ final class NestedParent() { case class NestedChild(name: String) } +object ObjectNestedParent { + case class ObjectNestedClass(name: String) +} + object MethodLocalWrapper { def methodLocalClassStringify: String = { diff --git a/tests/src/test/scala/pack/package.scala b/tests/src/test/scala/pack/package.scala new file mode 100644 index 0000000..3e72166 --- /dev/null +++ b/tests/src/test/scala/pack/package.scala @@ -0,0 +1,3 @@ +package object pack { + case class InPackageObject(name: String) +} From 380b6f620046100349ec633bae54cf5efc0bdb8b Mon Sep 17 00:00:00 2001 From: mrobakowski Date: Tue, 20 Apr 2021 21:45:27 +0200 Subject: [PATCH 2/7] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8edb4cc..a11f92f 100644 --- a/README.md +++ b/README.md @@ -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), its 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 :) From 513a573aae8bc598a31320a4add58c6bab663fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Robakowski?= Date: Tue, 20 Apr 2021 21:49:57 +0200 Subject: [PATCH 3/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a11f92f..4c536b1 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ 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 is nested in an object (but not a package object), its name and a dot are prepended. +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 From 411a2388e1d9c0b0ec26ec1cead37cadaf77479d Mon Sep 17 00:00:00 2001 From: mrobakowski Date: Wed, 21 Apr 2021 18:06:14 +0200 Subject: [PATCH 4/7] rename `ClazzParent` -> `EnclosingObject` --- .../main/scala-2/BetterToStringPlugin.scala | 8 ++++---- .../src/main/scala-2/Scala2CompilerApi.scala | 4 ++-- .../main/scala-3/BetterToStringPlugin.scala | 4 ++-- .../src/main/scala-3/Scala3CompilerApi.scala | 4 ++-- plugin/src/main/scala/BetterToStringImpl.scala | 18 +++++++++--------- tests/src/test/scala/Demo.scala | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/plugin/src/main/scala-2/BetterToStringPlugin.scala b/plugin/src/main/scala-2/BetterToStringPlugin.scala index 3588ede..841e136 100644 --- a/plugin/src/main/scala-2/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-2/BetterToStringPlugin.scala @@ -25,14 +25,14 @@ 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, parent: Option[ModuleDef]): Tree = + private def modifyClasses(tree: Tree, enclosingObject: Option[ModuleDef]): Tree = tree match { case p: PackageDef => p.copy(stats = p.stats.map(modifyClasses(_, None))) case m: ModuleDef => // couldn't find any nice api for this. `m.symbol.isPackageObject` does not work val isPackageObject = m.symbol.isInstanceOf[NoSymbol] && m.name.toString == "package" - val parent = if (!isPackageObject) Some(m) else None - m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, parent)))) + val enclosingObject = if (!isPackageObject) Some(m) else None + m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, enclosingObject)))) case clazz: ClassDef => impl.transformClass( clazz, @@ -40,7 +40,7 @@ final class BetterToStringPluginComponent(val global: Global) extends PluginComp // 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, - parent + enclosingObject ) case other => other } diff --git a/plugin/src/main/scala-2/Scala2CompilerApi.scala b/plugin/src/main/scala-2/Scala2CompilerApi.scala index d82cbb7..76c01f2 100644 --- a/plugin/src/main/scala-2/Scala2CompilerApi.scala +++ b/plugin/src/main/scala-2/Scala2CompilerApi.scala @@ -11,7 +11,7 @@ trait Scala2CompilerApi[G <: Global] extends CompilerApi { type Param = ValDef type ParamName = TermName type Method = DefDef - type ClazzParent = ModuleDef + type EnclosingObject = ModuleDef } object Scala2CompilerApi { @@ -26,7 +26,7 @@ object Scala2CompilerApi { } def className(clazz: Clazz): String = clazz.name.toString - def parentName(parent: ClazzParent) = parent.name.toString + def enclosingObjectName(enclosingObject: EnclosingObject) = 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" diff --git a/plugin/src/main/scala-3/BetterToStringPlugin.scala b/plugin/src/main/scala-3/BetterToStringPlugin.scala index 726e730..162ed04 100644 --- a/plugin/src/main/scala-3/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-3/BetterToStringPlugin.scala @@ -24,12 +24,12 @@ final class BetterToStringPluginPhase extends PluginPhase: val ownerOwner = ctx.owner.owner val isNested = !(ownerOwner.isPackageObject || ownerOwner.is(Module)) - val parent = if (ownerOwner.is(Module) && !ownerOwner.is(Package) && !ownerOwner.isPackageObject) + val enclosingObject = if (ownerOwner.is(Module) && !ownerOwner.is(Package) && !ownerOwner.isPackageObject) Some(ctx.owner.owner) else None BetterToStringImpl .instance(Scala3CompilerApi.instance) - .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, parent) + .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, enclosingObject) .t diff --git a/plugin/src/main/scala-3/Scala3CompilerApi.scala b/plugin/src/main/scala-3/Scala3CompilerApi.scala index fccdc0a..baa674d 100644 --- a/plugin/src/main/scala-3/Scala3CompilerApi.scala +++ b/plugin/src/main/scala-3/Scala3CompilerApi.scala @@ -20,7 +20,7 @@ trait Scala3CompilerApi extends CompilerApi: type Param = ValDef type ParamName = Names.TermName type Method = DefDef - type ClazzParent = Symbols.Symbol + type EnclosingObject = Symbols.Symbol object Scala3CompilerApi: final case class ClassContext(t: Template, clazz: ClassSymbol): @@ -33,7 +33,7 @@ object Scala3CompilerApi: } def className(clazz: Clazz): String = clazz.clazz.name.toString - def parentName(parent: ClazzParent): String = parent.effectiveName.toString + 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) diff --git a/plugin/src/main/scala/BetterToStringImpl.scala b/plugin/src/main/scala/BetterToStringImpl.scala index cef430c..9544746 100644 --- a/plugin/src/main/scala/BetterToStringImpl.scala +++ b/plugin/src/main/scala/BetterToStringImpl.scala @@ -8,10 +8,10 @@ trait CompilerApi { type Param type ParamName type Method - type ClazzParent + type EnclosingObject def className(clazz: Clazz): String - def parentName(parent: ClazzParent): String + def enclosingObjectName(enclosingObject: EnclosingObject): String def params(clazz: Clazz): List[Param] def literalConstant(value: String): Tree @@ -31,7 +31,7 @@ trait BetterToStringImpl[+C <: CompilerApi] { def transformClass( clazz: compilerApi.Clazz, isNested: Boolean, - parent: Option[compilerApi.ClazzParent] + enclosingObject: Option[compilerApi.EnclosingObject] ): compilerApi.Clazz } @@ -49,23 +49,23 @@ object BetterToStringImpl { def transformClass( clazz: Clazz, isNested: Boolean, - parent: Option[ClazzParent] + enclosingObject: Option[EnclosingObject] ): Clazz = { val hasToString: Boolean = methodNames(clazz).contains("toString") val shouldModify = isCaseClass(clazz) && !isNested && !hasToString - if (shouldModify) overrideToString(clazz, parent) + if (shouldModify) overrideToString(clazz, enclosingObject) else clazz } - private def overrideToString(clazz: Clazz, parent: Option[ClazzParent]): Clazz = - addMethod(clazz, createToString(clazz, toStringImpl(clazz, parent))) + private def overrideToString(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Clazz = + addMethod(clazz, createToString(clazz, toStringImpl(clazz, enclosingObject))) - private def toStringImpl(clazz: Clazz, parent: Option[ClazzParent]): Tree = { + private def toStringImpl(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Tree = { val className = api.className(clazz) - val parentPrefix = parent.map(p => api.parentName(p) ++ ".").getOrElse("") + val parentPrefix = enclosingObject.map(p => api.enclosingObjectName(p) ++ ".").getOrElse("") val paramListParts: List[Tree] = params(clazz).zipWithIndex.flatMap { case (v, index) => val commaPrefix = if (index > 0) ", " else "" diff --git a/tests/src/test/scala/Demo.scala b/tests/src/test/scala/Demo.scala index 9565c13..b35dacd 100644 --- a/tests/src/test/scala/Demo.scala +++ b/tests/src/test/scala/Demo.scala @@ -36,13 +36,13 @@ class Tests extends AnyWordSpec with Matchers { } "Class nested in an object" should { - "include parent's name" in { + "include enclosing object's name" in { ObjectNestedParent.ObjectNestedClass("Joe").toString shouldBe "ObjectNestedParent.ObjectNestedClass(name = Joe)" } } "Class nested in a package object" should { - "not include parent's name" in { + "not include package's name" in { pack.InPackageObject("Joe").toString shouldBe "InPackageObject(name = Joe)" } } From 8a4d40116d96c57980c4e30dad8fb432edcc20e4 Mon Sep 17 00:00:00 2001 From: mrobakowski Date: Sat, 1 May 2021 00:36:08 +0200 Subject: [PATCH 5/7] rebase and reformat --- plugin/src/main/resources/scalac-plugin.xml | 4 ++-- plugin/src/main/scala-3/BetterToStringPlugin.scala | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugin/src/main/resources/scalac-plugin.xml b/plugin/src/main/resources/scalac-plugin.xml index 3b76243..67eb16e 100644 --- a/plugin/src/main/resources/scalac-plugin.xml +++ b/plugin/src/main/resources/scalac-plugin.xml @@ -1,4 +1,4 @@ - better-tostring - com.kubukoz.BetterToStringPlugin + better-tostring + com.kubukoz.BetterToStringPlugin diff --git a/plugin/src/main/scala-3/BetterToStringPlugin.scala b/plugin/src/main/scala-3/BetterToStringPlugin.scala index 162ed04..67f54cd 100644 --- a/plugin/src/main/scala-3/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-3/BetterToStringPlugin.scala @@ -24,10 +24,11 @@ final class BetterToStringPluginPhase extends PluginPhase: val ownerOwner = ctx.owner.owner val isNested = !(ownerOwner.isPackageObject || ownerOwner.is(Module)) - val enclosingObject = if (ownerOwner.is(Module) && !ownerOwner.is(Package) && !ownerOwner.isPackageObject) - Some(ctx.owner.owner) - else - None + val enclosingObject = + if (ownerOwner.is(Module) && !ownerOwner.is(Package) && !ownerOwner.isPackageObject) + Some(ctx.owner.owner) + else + None BetterToStringImpl .instance(Scala3CompilerApi.instance) From 7ac685015a1d49bc5a9acbbe773d0eeadd42f7e6 Mon Sep 17 00:00:00 2001 From: mrobakowski Date: Sat, 1 May 2021 01:46:35 +0200 Subject: [PATCH 6/7] Add a test for deeply nested case classes and fix it not passing for Scala 3 --- .../main/scala-3/BetterToStringPlugin.scala | 24 +++++++++++++++---- tests/src/test/scala/Demo.scala | 12 ++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/scala-3/BetterToStringPlugin.scala b/plugin/src/main/scala-3/BetterToStringPlugin.scala index 67f54cd..91747fe 100644 --- a/plugin/src/main/scala-3/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-3/BetterToStringPlugin.scala @@ -4,11 +4,14 @@ 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" @@ -23,14 +26,25 @@ final class BetterToStringPluginPhase extends PluginPhase: val clazz = ctx.owner.asClass val ownerOwner = ctx.owner.owner - val isNested = !(ownerOwner.isPackageObject || ownerOwner.is(Module)) + val isNested = !(ownerOwner.isPackageObject || ownerOwner.is(Module)) || isAnyAncestorAClass(ownerOwner) + val enclosingObject = - if (ownerOwner.is(Module) && !ownerOwner.is(Package) && !ownerOwner.isPackageObject) - Some(ctx.owner.owner) - else - None + if ( + ownerOwner.is(Module) && + !ownerOwner.is(Package) && + !ownerOwner.isPackageObject + ) then Some(ctx.owner.owner) + else None BetterToStringImpl .instance(Scala3CompilerApi.instance) .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, enclosingObject) .t + + @tailrec private def isAnyAncestorAClass(sym: Symbols.Symbol)(using Context): Boolean = + if sym == Symbols.NoSymbol then return false + + // we want a class-class not an object (Module) or a package object (which are both also classes) + if !sym.is(Module) && !sym.is(Package) && !sym.isPackageObject then return true + + isAnyAncestorAClass(sym.owner) diff --git a/tests/src/test/scala/Demo.scala b/tests/src/test/scala/Demo.scala index b35dacd..d712593 100644 --- a/tests/src/test/scala/Demo.scala +++ b/tests/src/test/scala/Demo.scala @@ -53,6 +53,12 @@ class Tests extends AnyWordSpec with Matchers { } } + "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)" @@ -79,6 +85,12 @@ object ObjectNestedParent { case class ObjectNestedClass(name: String) } +final class DeeplyNestedInClassGrandparent { + object DeeplyNestedInClassParent { + case class DeeplyNestedInClassClass(name: String) + } +} + object MethodLocalWrapper { def methodLocalClassStringify: String = { From 25a6c43f63e7a2a354e811e7a7b596f6d375064f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 11 May 2021 03:33:30 +0200 Subject: [PATCH 7/7] Simplifications, move `isPackageObject` to compiler api for reuse --- .../src/main/scala-2/BetterToStringPlugin.scala | 5 +---- plugin/src/main/scala-2/Scala2CompilerApi.scala | 7 ++++++- .../src/main/scala-3/BetterToStringPlugin.scala | 16 ++-------------- plugin/src/main/scala-3/Scala3CompilerApi.scala | 9 ++++++++- plugin/src/main/scala/BetterToStringImpl.scala | 3 ++- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/plugin/src/main/scala-2/BetterToStringPlugin.scala b/plugin/src/main/scala-2/BetterToStringPlugin.scala index 841e136..225a4d4 100644 --- a/plugin/src/main/scala-2/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-2/BetterToStringPlugin.scala @@ -29,10 +29,7 @@ final class BetterToStringPluginComponent(val global: Global) extends PluginComp tree match { case p: PackageDef => p.copy(stats = p.stats.map(modifyClasses(_, None))) case m: ModuleDef => - // couldn't find any nice api for this. `m.symbol.isPackageObject` does not work - val isPackageObject = m.symbol.isInstanceOf[NoSymbol] && m.name.toString == "package" - val enclosingObject = if (!isPackageObject) Some(m) else None - m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, enclosingObject)))) + m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, Some(m))))) case clazz: ClassDef => impl.transformClass( clazz, diff --git a/plugin/src/main/scala-2/Scala2CompilerApi.scala b/plugin/src/main/scala-2/Scala2CompilerApi.scala index 76c01f2..4995313 100644 --- a/plugin/src/main/scala-2/Scala2CompilerApi.scala +++ b/plugin/src/main/scala-2/Scala2CompilerApi.scala @@ -26,7 +26,12 @@ object Scala2CompilerApi { } def className(clazz: Clazz): String = clazz.name.toString - def enclosingObjectName(enclosingObject: EnclosingObject) = enclosingObject.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" diff --git a/plugin/src/main/scala-3/BetterToStringPlugin.scala b/plugin/src/main/scala-3/BetterToStringPlugin.scala index 91747fe..1b4a6ce 100644 --- a/plugin/src/main/scala-3/BetterToStringPlugin.scala +++ b/plugin/src/main/scala-3/BetterToStringPlugin.scala @@ -26,25 +26,13 @@ final class BetterToStringPluginPhase extends PluginPhase: val clazz = ctx.owner.asClass val ownerOwner = ctx.owner.owner - val isNested = !(ownerOwner.isPackageObject || ownerOwner.is(Module)) || isAnyAncestorAClass(ownerOwner) + val isNested = ownerOwner.ownersIterator.exists(!_.is(Module)) val enclosingObject = - if ( - ownerOwner.is(Module) && - !ownerOwner.is(Package) && - !ownerOwner.isPackageObject - ) then Some(ctx.owner.owner) + if (ownerOwner.is(Module)) then Some(ownerOwner) else None BetterToStringImpl .instance(Scala3CompilerApi.instance) .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, enclosingObject) .t - - @tailrec private def isAnyAncestorAClass(sym: Symbols.Symbol)(using Context): Boolean = - if sym == Symbols.NoSymbol then return false - - // we want a class-class not an object (Module) or a package object (which are both also classes) - if !sym.is(Module) && !sym.is(Package) && !sym.isPackageObject then return true - - isAnyAncestorAClass(sym.owner) diff --git a/plugin/src/main/scala-3/Scala3CompilerApi.scala b/plugin/src/main/scala-3/Scala3CompilerApi.scala index baa674d..816374a 100644 --- a/plugin/src/main/scala-3/Scala3CompilerApi.scala +++ b/plugin/src/main/scala-3/Scala3CompilerApi.scala @@ -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 @@ -33,7 +34,13 @@ object Scala3CompilerApi: } def className(clazz: Clazz): String = clazz.clazz.name.toString - def enclosingObjectName(enclosingObject: EnclosingObject): String = enclosingObject.effectiveName.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) diff --git a/plugin/src/main/scala/BetterToStringImpl.scala b/plugin/src/main/scala/BetterToStringImpl.scala index 9544746..be0de9f 100644 --- a/plugin/src/main/scala/BetterToStringImpl.scala +++ b/plugin/src/main/scala/BetterToStringImpl.scala @@ -11,6 +11,7 @@ trait CompilerApi { 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 @@ -65,7 +66,7 @@ object BetterToStringImpl { private def toStringImpl(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Tree = { val className = api.className(clazz) - val parentPrefix = enclosingObject.map(p => api.enclosingObjectName(p) ++ ".").getOrElse("") + 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 ""