Skip to content

Commit 8968458

Browse files
committed
Improve message for "null toString" per review
Also refactor the stringOf reflection to lookup method once. Add worst-case fallback to use String.valueOf. Re-enable some tests which appear not to crash. [Cherry-picked 302aaca][modified]
1 parent 3cbef4d commit 8968458

File tree

2 files changed

+75
-66
lines changed

2 files changed

+75
-66
lines changed

compiler/src/dotty/tools/repl/Rendering.scala

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -49,39 +49,40 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
4949
// We need to use the ScalaRunTime class coming from the scala-library
5050
// on the user classpath, and not the one available in the current
5151
// classloader, so we use reflection instead of simply calling
52-
// `ScalaRunTime.replStringOf`. Probe for new API without extraneous newlines.
53-
// For old API, try to clean up extraneous newlines by stripping suffix and maybe prefix newline.
52+
// `ScalaRunTime.stringOf`. Also probe for new stringOf that does string quoting, etc.
5453
val scalaRuntime = Class.forName("scala.runtime.ScalaRunTime", true, myClassLoader)
5554
val renderer = "stringOf"
56-
def stringOfMaybeTruncated(value: Object, maxElements: Int): String = {
57-
try {
58-
val meth = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int], classOf[Boolean])
59-
val truly = java.lang.Boolean.TRUE
60-
meth.invoke(null, value, maxElements, truly).asInstanceOf[String]
61-
} catch {
62-
case _: NoSuchMethodException =>
63-
val meth = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int])
64-
meth.invoke(null, value, maxElements).asInstanceOf[String]
65-
}
66-
}
67-
68-
(value: Object, maxElements: Int, maxCharacters: Int) => {
69-
// `ScalaRuntime.stringOf` may truncate the output, in which case we want to indicate that fact to the user
70-
// In order to figure out if it did get truncated, we invoke it twice - once with the `maxElements` that we
71-
// want to print, and once without a limit. If the first is shorter, truncation did occur.
72-
val notTruncated = stringOfMaybeTruncated(value, Int.MaxValue)
73-
if notTruncated == null then null else
74-
val maybeTruncated =
75-
val maybeTruncatedByElementCount = stringOfMaybeTruncated(value, maxElements)
76-
truncate(maybeTruncatedByElementCount, maxCharacters)
77-
78-
// our string representation may have been truncated by element and/or character count
79-
// if so, append an info string - but only once
80-
if notTruncated.length == maybeTruncated.length then maybeTruncated
81-
else s"$maybeTruncated ... large output truncated, print value to show all"
82-
end if
83-
}
84-
55+
val stringOfInvoker: (Object, Int) => String =
56+
def richStringOf: (Object, Int) => String =
57+
val method = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int], classOf[Boolean])
58+
val richly = java.lang.Boolean.TRUE // add a repl option for enriched output
59+
(value, maxElements) => method.invoke(null, value, maxElements, richly).asInstanceOf[String]
60+
def poorStringOf: (Object, Int) => String =
61+
try
62+
val method = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int])
63+
(value, maxElements) => method.invoke(null, value, maxElements).asInstanceOf[String]
64+
catch case _: NoSuchMethodException => (value, maxElements) => String.valueOf(value).take(maxElements)
65+
try richStringOf
66+
catch case _: NoSuchMethodException => poorStringOf
67+
def stringOfMaybeTruncated(value: Object, maxElements: Int): String = stringOfInvoker(value, maxElements)
68+
69+
// require value != null
70+
// `ScalaRuntime.stringOf` returns null iff value.toString == null, let caller handle that.
71+
// `ScalaRuntime.stringOf` may truncate the output, in which case we want to indicate that fact to the user
72+
// In order to figure out if it did get truncated, we invoke it twice - once with the `maxElements` that we
73+
// want to print, and once without a limit. If the first is shorter, truncation did occur.
74+
// Note that `stringOf` has new API in flight to handle truncation, see stringOfMaybeTruncated.
75+
(value: Object, maxElements: Int, maxCharacters: Int) =>
76+
stringOfMaybeTruncated(value, Int.MaxValue) match
77+
case null => null
78+
case notTruncated =>
79+
val maybeTruncated =
80+
val maybeTruncatedByElementCount = stringOfMaybeTruncated(value, maxElements)
81+
truncate(maybeTruncatedByElementCount, maxCharacters)
82+
// our string representation may have been truncated by element and/or character count
83+
// if so, append an info string - but only once
84+
if notTruncated.length == maybeTruncated.length then maybeTruncated
85+
else s"$maybeTruncated ... large output truncated, print value to show all"
8586
}
8687
myClassLoader
8788
}
@@ -92,14 +93,20 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
9293
else str.substring(0, str.offsetByCodePoints(0, maxPrintCharacters - 1))
9394

9495
/** Return a String representation of a value we got from `classLoader()`. */
95-
private[repl] def replStringOf(value: Object)(using Context): String =
96+
private[repl] def replStringOf(sym: Symbol, value: Object)(using Context): String =
9697
assert(myReplStringOf != null,
9798
"replStringOf should only be called on values creating using `classLoader()`, but `classLoader()` has not been called so far")
9899
val maxPrintElements = ctx.settings.VreplMaxPrintElements.valueIn(ctx.settingsState)
99100
val maxPrintCharacters = ctx.settings.VreplMaxPrintCharacters.valueIn(ctx.settingsState)
100-
Option(value)
101-
.flatMap(v => Option(myReplStringOf(v, maxPrintElements, maxPrintCharacters)))
102-
.getOrElse("null // non-null reference has null-valued toString")
101+
// stringOf returns null if value.toString returns null. Show some text as a fallback.
102+
def toIdentityString(value: Object): String =
103+
s"${value.getClass.getName}@${System.identityHashCode(value).toHexString}"
104+
def fallback = s"""${toIdentityString(value)} // return value of "${sym.name}.toString" is null"""
105+
if value == null then "null" else
106+
myReplStringOf(value, maxPrintElements, maxPrintCharacters) match
107+
case null => fallback
108+
case res => res
109+
end if
103110

104111
/** Load the value of the symbol using reflection.
105112
*
@@ -111,17 +118,15 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
111118
val symValue = resObj
112119
.getDeclaredMethods.find(_.getName == sym.name.encode.toString)
113120
.flatMap(result => rewrapValueClass(sym.info.classSymbol, result.invoke(null)))
114-
val valueString = symValue.map(replStringOf)
121+
symValue
122+
.filter(_ => sym.is(Flags.Method) || sym.info != defn.UnitType)
123+
.map(value => stripReplPrefix(replStringOf(sym, value)))
115124

116-
if (!sym.is(Flags.Method) && sym.info == defn.UnitType)
117-
None
125+
private def stripReplPrefix(s: String): String =
126+
if (s.startsWith(REPL_WRAPPER_NAME_PREFIX))
127+
s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$')
118128
else
119-
valueString.map { s =>
120-
if (s.startsWith(REPL_WRAPPER_NAME_PREFIX))
121-
s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$')
122-
else
123-
s
124-
}
129+
s
125130

126131
/** Rewrap value class to their Wrapper class
127132
*

compiler/test/dotty/tools/repl/ReplCompilerTests.scala

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import scala.language.unsafeNulls
44

55
import java.util.regex.Pattern
66

7-
import org.junit.Assert.{assertTrue => assert, _}
8-
import org.junit.{Ignore, Test}
7+
import org.junit.Assert.{assertEquals, assertFalse, assertTrue}
8+
import org.junit.Assert.{assertTrue => assert}
9+
import org.junit.Test
910
import dotty.tools.dotc.core.Contexts.Context
1011

1112
class ReplCompilerTests extends ReplTest:
@@ -107,28 +108,21 @@ class ReplCompilerTests extends ReplTest:
107108
assertEquals(expected, lines())
108109
}
109110

110-
// FIXME: Tests are not run in isolation, the classloader is corrupted after the first exception
111-
@Ignore @Test def i3305: Unit = {
112-
initially {
113-
run("null.toString")
114-
assert(storedOutput().startsWith("java.lang.NullPointerException"))
115-
}
111+
@Test def `i3305 SOE meh`: Unit = initially:
112+
run("def foo: Int = 1 + foo; foo")
113+
assert(storedOutput().startsWith("java.lang.StackOverflowError"))
116114

117-
initially {
118-
run("def foo: Int = 1 + foo; foo")
119-
assert(storedOutput().startsWith("def foo: Int\njava.lang.StackOverflowError"))
120-
}
115+
@Test def `i3305 NPE`: Unit = initially:
116+
run("null.toString")
117+
assert(storedOutput().startsWith("java.lang.NullPointerException"))
121118

122-
initially {
123-
run("""throw new IllegalArgumentException("Hello")""")
124-
assert(storedOutput().startsWith("java.lang.IllegalArgumentException: Hello"))
125-
}
119+
@Test def `i3305 IAE`: Unit = initially:
120+
run("""throw new IllegalArgumentException("Hello")""")
121+
assertTrue(storedOutput().startsWith("java.lang.IllegalArgumentException: Hello"))
126122

127-
initially {
128-
run("val (x, y) = null")
129-
assert(storedOutput().startsWith("scala.MatchError: null"))
130-
}
131-
}
123+
@Test def `i3305 ME`: Unit = initially:
124+
run("val (x, y) = null")
125+
assert(storedOutput().startsWith("scala.MatchError: null"))
132126

133127
@Test def i2789: Unit = initially {
134128
run("(x: Int) => println(x)")
@@ -357,8 +351,18 @@ class ReplCompilerTests extends ReplTest:
357351
initially:
358352
run("val tpolecat = new Object { override def toString(): String = null }")
359353
.andThen:
360-
assertEquals("val tpolecat: Object = null // non-null reference has null-valued toString", lines().head)
354+
val last = lines().last
355+
assertTrue(last, last.startsWith("val tpolecat: Object = anon"))
356+
assertTrue(last, last.endsWith("""// return value of "tpolecat.toString" is null"""))
361357

358+
@Test def `i17333 print toplevel object with null toString`: Unit =
359+
initially:
360+
run("object tpolecat { override def toString(): String = null }")
361+
.andThen:
362+
run("tpolecat")
363+
val last = lines().last
364+
assertTrue(last, last.startsWith("val res0: tpolecat.type = tpolecat"))
365+
assertTrue(last, last.endsWith("""// return value of "res0.toString" is null"""))
362366
end ReplCompilerTests
363367

364368
object ReplCompilerTests:

0 commit comments

Comments
 (0)