Skip to content

Commit eb277c8

Browse files
committed
Fixed: ScriptREPL is not usable - problem with variables named as Scala keywords #9
1 parent 2773bce commit eb277c8

File tree

2 files changed

+145
-20
lines changed

2 files changed

+145
-20
lines changed

src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala

+104-20
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,40 @@
1+
/*
2+
* #%L
3+
* JSR-223-compliant Scala scripting language plugin.
4+
* %%
5+
* Copyright (C) 2013 - 2023 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
130
package org.scijava.plugins.scripting.scala
231

332
import java.io.{OutputStream, Reader, StringWriter, Writer}
433
import javax.script.*
534
import scala.collection.mutable
635
import scala.jdk.CollectionConverters.*
736
import scala.util.Try
37+
import scala.util.matching.Regex
838

939
/**
1040
* Adapted Scala ScriptEngine
@@ -43,26 +73,30 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin
4373
bindings <- Option(context.getBindings(scope)).map(_.asScala) // bindings in context can be null
4474
yield {
4575
for (name, value) <- bindings yield {
46-
value match
47-
case v: Double => s"val $name : Double = ${v}d"
48-
case v: Float => s"val $name : Float = ${v}f"
49-
case v: Long => s"val $name : Long = ${v}L"
50-
case v: Int => s"val $name : Int = $v"
51-
case v: Char => s"val $name : Char = '$v'"
52-
case v: Short => s"val $name : Short = $v"
53-
case v: Byte => s"val $name : Byte = $v"
54-
case v: Boolean => s"val $name : Int = $v"
55-
case o: AnyRef if isValidVariableName(name) =>
56-
_transfer = o
57-
val typeName = Option(o).map(_.getClass.getCanonicalName).getOrElse("AnyRef")
58-
s"""
59-
|val $name : $typeName = {
60-
| val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine._transfer
61-
| t.asInstanceOf[$typeName]
62-
|}""".stripMargin
63-
case _: AnyRef => "" // ignore if name is not a variable
64-
case v: Unit =>
65-
throw ScriptException(s"Unsupported type for bind variable $name: ${v.getClass}")
76+
if isValidVariableName(name) then
77+
val validName = addBackticksIfNeeded(name)
78+
value match
79+
case v: Double => s"val $validName : Double = ${v}d"
80+
case v: Float => s"val $validName : Float = ${v}f"
81+
case v: Long => s"val $validName : Long = ${v}L"
82+
case v: Int => s"val $validName : Int = $v"
83+
case v: Char => s"val $validName : Char = '$v'"
84+
case v: Short => s"val $validName : Short = $v"
85+
case v: Byte => s"val $validName : Byte = $v"
86+
case v: Boolean => s"val $validName : Int = $v"
87+
case v: AnyRef =>
88+
_transfer = v
89+
val typeName = Option(v).map(_.getClass.getCanonicalName).getOrElse("AnyRef")
90+
val validTypeName = addBackticksIfNeeded(typeName)
91+
s"""
92+
|val $validName : $validTypeName = {
93+
| val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine._transfer
94+
| t.asInstanceOf[$validTypeName]
95+
|}""".stripMargin
96+
case v: Unit =>
97+
throw ScriptException(s"Unsupported type for bind variable $name: ${v.getClass}")
98+
else
99+
"" // ignore if name is not a variable
66100
}
67101
}
68102

@@ -126,10 +160,60 @@ end ScalaAdaptedScriptEngine
126160

127161
object ScalaAdaptedScriptEngine:
128162
private lazy val variableNamePattern = """^[a-zA-Z_$][a-zA-Z_$0-9]*$""".r
163+
private val scala3Keywords = Seq(
164+
"abstract",
165+
"case",
166+
"catch",
167+
"class",
168+
"def",
169+
"do",
170+
"else",
171+
"enum",
172+
"export",
173+
"extends",
174+
"false",
175+
"final",
176+
"finally",
177+
"for",
178+
"given",
179+
"if",
180+
"implicit",
181+
"import",
182+
"lazy",
183+
"match",
184+
"new",
185+
"null",
186+
"object",
187+
"override",
188+
"package",
189+
"private",
190+
"protected",
191+
"return",
192+
"sealed",
193+
"super",
194+
"then",
195+
"throw",
196+
"trait",
197+
"true",
198+
"try",
199+
"type",
200+
"val",
201+
"var",
202+
"while",
203+
"with",
204+
"yield"
205+
)
129206

130207
/** Do not use externally despite it is declared public. IT is public so it is accessible from scripts */
131208
// noinspection ScalaWeakerAccess
132209
var _transfer: Object = _
133210

134211
private def isValidVariableName(name: String): Boolean = variableNamePattern.matches(name)
212+
213+
private[scala] def addBackticksIfNeeded(referenceName: String): String =
214+
referenceName
215+
.split("\\.")
216+
.map(n => if scala3Keywords.contains(n) then s"`$n`" else n)
217+
.mkString(".")
218+
135219
end ScalaAdaptedScriptEngine

src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java

+41
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
import javax.script.ScriptContext;
3939
import javax.script.ScriptEngine;
40+
import javax.script.ScriptException;
4041
import javax.script.SimpleScriptContext;
4142
import java.io.StringWriter;
4243

@@ -286,4 +287,44 @@ public void issue5() {
286287
assertEquals(20, engine.get("twenty"));
287288
}
288289
}
290+
291+
/**
292+
* Test for issue #9: "ScriptREPL is not usable - problem with variables named as Scala keywords"
293+
*/
294+
@Test
295+
public void issue9() {
296+
try (final Context context = new Context(ScriptService.class)) {
297+
298+
final ScriptService scriptService = context.getService(ScriptService.class);
299+
final ScriptEngine engine = scriptService.getLanguageByName("scala").getScriptEngine();
300+
301+
final String name = "object";
302+
final int expectedValue = 7;
303+
304+
assertFalse(engine.getBindings(ScriptContext.ENGINE_SCOPE).containsKey(name));
305+
engine.put(name, expectedValue);
306+
assertTrue(engine.getBindings(ScriptContext.ENGINE_SCOPE).containsKey(name));
307+
assertEquals(expectedValue, engine.get(name));
308+
309+
final String script1 = "val abc = `" + name + "`";
310+
try {
311+
engine.eval(script1);
312+
} catch (ScriptException e) {
313+
fail("Failed to execute valid script: \"" + script1 + "\"");
314+
}
315+
316+
final Object r = engine.get("abc");
317+
assertEquals(expectedValue, r);
318+
319+
}
320+
}
321+
322+
@Test
323+
public void addBackticksIfNeededTest() {
324+
final String expected = "org.scijava.`object`.DefaultObjectService";
325+
final String name = "org.scijava.object.DefaultObjectService";
326+
final String correctedName = ScalaAdaptedScriptEngine.addBackticksIfNeeded(name);
327+
328+
assertEquals(expected, correctedName);
329+
}
289330
}

0 commit comments

Comments
 (0)