Skip to content

Commit 655b7d9

Browse files
committed
Put REPL wrappers in a (special) package
Prior to this commit, the REPL wrapper objects were placed directly in the empty package. This prevented definitions in the REPL session from shadowing definitions in the empty package on the classpath (e.g. from stray .class files in the current directory).
1 parent 6972f8e commit 655b7d9

File tree

6 files changed

+153
-17
lines changed

6 files changed

+153
-17
lines changed

compiler/src/dotty/tools/dotc/core/StdNames.scala

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ object StdNames {
133133
val OPS_PACKAGE: N = "<special-ops>"
134134
val OVERLOADED: N = "<overloaded>"
135135
val PACKAGE: N = "package"
136+
val REPL_PACKAGE: N = "repl$"
136137
val ROOT: N = "<root>"
137138
val SPECIALIZED_SUFFIX: N = "$sp"
138139
val SUPER_PREFIX: N = "super$"

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
118118
None
119119
else
120120
string.map { s =>
121-
if (s.startsWith(str.REPL_SESSION_LINE))
122-
s.drop(str.REPL_SESSION_LINE.length).dropWhile(c => c.isDigit || c == '$')
121+
if (s.startsWith(REPL_WRAPPER_NAME_PREFIX))
122+
s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$')
123123
else
124124
s
125125
}
@@ -180,6 +180,7 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
180180
}
181181

182182
object Rendering {
183+
final val REPL_WRAPPER_NAME_PREFIX = s"${nme.REPL_PACKAGE}.${str.REPL_SESSION_LINE}"
183184

184185
extension (s: Symbol)
185186
def showUser(using Context): String = {

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ReplCompiler extends Compiler {
4646

4747
def importPreviousRun(id: Int)(using Context) = {
4848
// we first import the wrapper object id
49-
val path = nme.EMPTY_PACKAGE ++ "." ++ objectNames(id)
49+
val path = nme.REPL_PACKAGE ++ "." ++ objectNames(id)
5050
val ctx0 = ctx.fresh
5151
.setNewScope
5252
.withRootImports(RootRef(() => requiredModuleRef(path)) :: Nil)
@@ -58,7 +58,9 @@ class ReplCompiler extends Compiler {
5858
importContext(imp)(using ctx))
5959
}
6060

61-
val rootCtx = super.rootContext.withRootImports
61+
val rootCtx = super.rootContext
62+
.withRootImports // default root imports
63+
.withRootImports(RootRef(() => defn.EmptyPackageVal.termRef) :: Nil)
6264
(1 to state.objectIndex).foldLeft(rootCtx)((ctx, id) =>
6365
importPreviousRun(id)(using ctx))
6466
}
@@ -130,7 +132,7 @@ class ReplCompiler extends Compiler {
130132
val module = ModuleDef(objectTermName, tmpl)
131133
.withSpan(span)
132134

133-
PackageDef(Ident(nme.EMPTY_PACKAGE), List(module))
135+
PackageDef(Ident(nme.REPL_PACKAGE), List(module))
134136
}
135137

136138
private def createUnit(defs: Definitions, span: Span)(using Context): CompilationUnit = {
@@ -231,7 +233,7 @@ class ReplCompiler extends Compiler {
231233
val wrapper = TypeDef("$wrapper".toTypeName, tmpl)
232234
.withMods(Modifiers(Final))
233235
.withSpan(Span(0, expr.length))
234-
PackageDef(Ident(nme.EMPTY_PACKAGE), List(wrapper))
236+
PackageDef(Ident(nme.REPL_PACKAGE), List(wrapper))
235237
}
236238

237239
ParseResult(sourceFile)(state) match {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package repl
33

44
import java.io.{Reader, StringWriter}
55
import javax.script.{AbstractScriptEngine, Bindings, ScriptContext, ScriptEngine => JScriptEngine, ScriptEngineFactory, ScriptException, SimpleBindings}
6-
import dotc.core.StdNames.str
6+
import dotc.core.StdNames.{nme, str}
77

88
/** A JSR 223 (Scripting API) compatible wrapper around the REPL for improved
99
* interoperability with software that supports it.
@@ -37,7 +37,7 @@ class ScriptEngine extends AbstractScriptEngine {
3737
val vid = state.valIndex
3838
state = driver.run(script)(state)
3939
val oid = state.objectIndex
40-
Class.forName(s"${str.REPL_SESSION_LINE}$oid", true, rendering.classLoader()(using state.context))
40+
Class.forName(s"${nme.REPL_PACKAGE}.${str.REPL_SESSION_LINE}$oid", true, rendering.classLoader()(using state.context))
4141
.getDeclaredMethods.find(_.getName == s"${str.REPL_RES_PREFIX}$vid")
4242
.map(_.invoke(null))
4343
.getOrElse(null)

compiler/test-resources/repl/i7635

-9
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package dotty.tools
2+
package repl
3+
4+
import java.io.File
5+
import java.nio.file.{Path, Files}
6+
import java.util.Comparator
7+
8+
import org.junit.{Test, Ignore, BeforeClass, AfterClass}
9+
10+
import dotc.Driver
11+
import dotc.reporting.TestReporter
12+
import dotc.interfaces.Diagnostic.ERROR
13+
import vulpix.{TestConfiguration, TestFlags}
14+
15+
/** Test that the REPL can shadow artifacts in the local filesystem on the classpath.
16+
* Since the REPL launches with the current directory on the classpath, stray .class
17+
* files containing definitions in the empty package will be in scope in the REPL.
18+
* Additionally, any subdirectories will be treated as package names in scope.
19+
* As this may come as a surprise to an unsuspecting user, we would like definitions
20+
* from the REPL session to shadow these names.
21+
*
22+
* Provided here is a framework for creating the filesystem artifacts to be shadowed
23+
* and running scripted REPL tests with them on the claspath.
24+
*/
25+
object ShadowingTests:
26+
def classpath = TestConfiguration.basicClasspath + File.pathSeparator + shadowDir
27+
def options = ReplTest.commonOptions ++ Array("-classpath", classpath)
28+
def shadowDir = dir.toAbsolutePath.toString
29+
30+
def createSubDir(name: String): Path =
31+
val subdir = dir.resolve(name)
32+
try Files.createDirectory(subdir)
33+
catch case _: java.nio.file.FileAlreadyExistsException =>
34+
assert(Files.isDirectory(subdir), s"failed to create shadowed subdirectory $subdir")
35+
subdir
36+
37+
// The directory on the classpath containing artifacts to be shadowed
38+
private var dir: Path = null
39+
40+
@BeforeClass def setupDir: Unit =
41+
dir = Files.createTempDirectory("repl-shadow")
42+
43+
@AfterClass def tearDownDir: Unit =
44+
Files.walk(dir).sorted(Comparator.reverseOrder).forEach(Files.delete)
45+
dir = null
46+
47+
class ShadowingTests extends ReplTest(options = ShadowingTests.options):
48+
// delete contents of shadowDir after each test
49+
override def cleanup: Unit =
50+
super.cleanup
51+
val dir = ShadowingTests.dir
52+
Files.walk(dir)
53+
.filter(_ != dir)
54+
.sorted(Comparator.reverseOrder)
55+
.forEach(Files.delete)
56+
57+
/** Run a scripted REPL test with the compilation artifacts of `shadowed` on the classpath */
58+
def shadowedScriptedTest(name: String, shadowed: String, script: String): Unit =
59+
compileShadowed(shadowed)
60+
testScript(name, script.linesIterator.toList)
61+
62+
/** Compile the given source text and output to the shadow dir on the classpath */
63+
private def compileShadowed(src: String): Unit =
64+
val file: Path = Files.createTempFile("repl-shadow-test", ".scala")
65+
Files.write(file, src.getBytes)
66+
67+
val flags =
68+
TestFlags(TestConfiguration.basicClasspath, TestConfiguration.noCheckOptions)
69+
.and("-d", ShadowingTests.shadowDir)
70+
val driver = new Driver
71+
val reporter = TestReporter.reporter(System.out, logLevel = ERROR)
72+
driver.process(flags.all :+ file.toString, reporter)
73+
assert(!reporter.hasErrors, s"compilation of $file failed")
74+
Files.delete(file)
75+
end compileShadowed
76+
77+
@Test def i7635 = shadowedScriptedTest(name = "<i7635>",
78+
shadowed = "class C(val c: Int)",
79+
script =
80+
"""|scala> new C().c
81+
|1 | new C().c
82+
| | ^^^^^^^
83+
| | missing argument for parameter c of constructor C in class C: (c: Int): C
84+
|
85+
|scala> new C(13).c
86+
|val res0: Int = 13
87+
|
88+
|scala> class C { val c = 42 }
89+
|// defined class C
90+
|
91+
|scala> new C().c
92+
|val res1: Int = 42
93+
|""".stripMargin
94+
)
95+
96+
@Ignore("not yet fixed")
97+
@Test def `shadow subdirectories on classpath` =
98+
// NB: Tests of shadowing of subdirectories on the classpath are only valid
99+
// when the subdirectories exist prior to initialization of the REPL driver.
100+
// In the tests below this is enforced by the call to `testScript` which
101+
// in turn invokes `ReplDriver#resetToInitial`. When testing interactively,
102+
// the subdirectories may be created before launching the REPL, or during
103+
// an existing session followed by the `:reset` command.
104+
105+
ShadowingTests.createSubDir("foo")
106+
testScript(name = "<shadow-subdir-foo>",
107+
"""|scala> val foo = 3
108+
|val foo: Int = 3
109+
|
110+
|scala> foo
111+
|val res0: Int = 3
112+
|""".stripMargin.linesIterator.toList
113+
)
114+
115+
ShadowingTests.createSubDir("x")
116+
testScript(name = "<shadow-subdir-x>",
117+
"""|scala> val (x, y) = (42, "foo")
118+
|val x: Int = 42
119+
|val y: String = foo
120+
|
121+
|scala> if (true) x else y
122+
|val res0: Matchable = 42
123+
|""".stripMargin.linesIterator.toList
124+
)
125+
126+
ShadowingTests.createSubDir("util")
127+
testScript(name = "<shadow-subdir-util>",
128+
"""|scala> import util.Try
129+
|1 | import util.Try
130+
| | ^^^
131+
| | value Try is not a member of util
132+
|
133+
|scala> object util { class Try { override def toString = "you've gotta try!" } }
134+
|// defined object util
135+
|
136+
|scala> import util.Try
137+
|scala> new Try
138+
|val res0: util.Try = you've gotta try!
139+
|""".stripMargin.linesIterator.toList
140+
)
141+
end ShadowingTests

0 commit comments

Comments
 (0)