Skip to content

Commit d9799a8

Browse files
authored
Merge pull request #12607 from griggt/package-repl-wrappers
Fix #7635: Package REPL wrappers
2 parents d28b891 + 655b7d9 commit d9799a8

File tree

7 files changed

+160
-23
lines changed

7 files changed

+160
-23
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.

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na
3939
extension [A](state: State)
4040
def andThen(op: State => A): A = op(state)
4141

42-
def testFile(f: JFile): Unit = {
42+
def testFile(f: JFile): Unit = testScript(f.toString, readLines(f))
43+
44+
def testScript(name: => String, lines: List[String]): Unit = {
4345
val prompt = "scala>"
4446

4547
def evaluate(state: State, input: String) =
@@ -50,7 +52,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na
5052
}
5153
catch {
5254
case ex: Throwable =>
53-
System.err.println(s"failed while running script: $f, on:\n$input")
55+
System.err.println(s"failed while running script: $name, on:\n$input")
5456
throw ex
5557
}
5658

@@ -60,13 +62,12 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na
6062
case nonEmptyLine => nonEmptyLine :: Nil
6163
}
6264

63-
val expectedOutput = readLines(f).flatMap(filterEmpties)
65+
val expectedOutput = lines.flatMap(filterEmpties)
6466
val actualOutput = {
6567
resetToInitial()
6668

67-
val lines = readLines(f)
6869
assert(lines.head.startsWith(prompt),
69-
s"""Each file has to start with the prompt: "$prompt"""")
70+
s"""Each script must start with the prompt: "$prompt"""")
7071
val inputRes = lines.filter(_.startsWith(prompt))
7172

7273
val buf = new ArrayBuffer[String]
@@ -88,7 +89,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na
8889
println("actual ===========>")
8990
println(actualOutput.mkString(EOL))
9091

91-
fail(s"Error in file $f, expected output did not match actual")
92+
fail(s"Error in script $name, expected output did not match actual")
9293
end if
9394
}
9495
}
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)