Skip to content

Commit 2f368f4

Browse files
Backport "Repl truncation copes with null" to LTS (#20969)
Backports #17336 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents 53febd4 + eabf647 commit 2f368f4

File tree

2 files changed

+79
-61
lines changed

2 files changed

+79
-61
lines changed

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

+47-40
Original file line numberDiff line numberDiff line change
@@ -49,36 +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-
val maybeTruncatedByElementCount = stringOfMaybeTruncated(value, maxElements)
74-
val maybeTruncated = truncate(maybeTruncatedByElementCount, maxCharacters)
75-
76-
// our string representation may have been truncated by element and/or character count
77-
// if so, append an info string - but only once
78-
if (notTruncated.length == maybeTruncated.length) maybeTruncated
79-
else s"$maybeTruncated ... large output truncated, print value to show all"
80-
}
81-
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"
8286
}
8387
myClassLoader
8488
}
@@ -89,13 +93,18 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
8993
else str.substring(0, str.offsetByCodePoints(0, maxPrintCharacters - 1))
9094

9195
/** Return a String representation of a value we got from `classLoader()`. */
92-
private[repl] def replStringOf(value: Object)(using Context): String =
96+
private[repl] def replStringOf(sym: Symbol, value: Object)(using Context): String =
9397
assert(myReplStringOf != null,
9498
"replStringOf should only be called on values creating using `classLoader()`, but `classLoader()` has not been called so far")
9599
val maxPrintElements = ctx.settings.VreplMaxPrintElements.valueIn(ctx.settingsState)
96100
val maxPrintCharacters = ctx.settings.VreplMaxPrintCharacters.valueIn(ctx.settingsState)
97-
val res = myReplStringOf(value, maxPrintElements, maxPrintCharacters)
98-
if res == null then "null // non-null reference has null-valued toString" else res
101+
// stringOf returns null if value.toString returns null. Show some text as a fallback.
102+
def fallback = s"""null // result of "${sym.name}.toString" is null"""
103+
if value == null then "null" else
104+
myReplStringOf(value, maxPrintElements, maxPrintCharacters) match
105+
case null => fallback
106+
case res => res
107+
end if
99108

100109
/** Load the value of the symbol using reflection.
101110
*
@@ -107,17 +116,15 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
107116
val symValue = resObj
108117
.getDeclaredMethods.find(_.getName == sym.name.encode.toString)
109118
.flatMap(result => rewrapValueClass(sym.info.classSymbol, result.invoke(null)))
110-
val valueString = symValue.map(replStringOf)
119+
symValue
120+
.filter(_ => sym.is(Flags.Method) || sym.info != defn.UnitType)
121+
.map(value => stripReplPrefix(replStringOf(sym, value)))
111122

112-
if (!sym.is(Flags.Method) && sym.info == defn.UnitType)
113-
None
123+
private def stripReplPrefix(s: String): String =
124+
if (s.startsWith(REPL_WRAPPER_NAME_PREFIX))
125+
s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$')
114126
else
115-
valueString.map { s =>
116-
if (s.startsWith(REPL_WRAPPER_NAME_PREFIX))
117-
s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$')
118-
else
119-
s
120-
}
127+
s
121128

122129
/** Rewrap value class to their Wrapper class
123130
*

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

+32-21
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)")
@@ -353,6 +347,23 @@ class ReplCompilerTests extends ReplTest:
353347
@Test def `i13097 expect template after colon` = contextually:
354348
assert(ParseResult.isIncomplete("class C:"))
355349

350+
@Test def `i17333 print null result of toString`: Unit =
351+
initially:
352+
run("val tpolecat = new Object { override def toString(): String = null }")
353+
.andThen:
354+
val last = lines().last
355+
assertTrue(last, last.startsWith("val tpolecat: Object = null"))
356+
assertTrue(last, last.endsWith("""// result of "tpolecat.toString" is null"""))
357+
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 = null"))
365+
assertTrue(last, last.endsWith("""// result of "res0.toString" is null"""))
366+
356367
object ReplCompilerTests:
357368

358369
private val pattern = Pattern.compile("\\r[\\n]?|\\n");

0 commit comments

Comments
 (0)