diff --git a/.gitignore b/.gitignore index 08c660b..a4fd068 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ # Maven # target/ + +# IntelliJ IDEA # +/.idea/ diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..36dc7b5 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,14 @@ +# [[https://github.com/scalameta/scalafmt]] + +version = 3.7.3 + +runner.dialect = scala3 + +preset = IntelliJ +align.preset = more +maxColumn = 120 +docstrings.style = Asterisk +docstrings.blankFirstLine = yes +docstrings.wrap = no +importSelectors = singleLine +newlines.source = keep \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index 08db1a8..8082288 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2013 - 2023, SciJava developers. +Copyright (c) 2013 - 2024, SciJava developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/pom.xml b/pom.xml index a258a05..6d7955b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ org.scijava pom-scijava - 34.1.0 + 35.1.0 scripting-scala - 0.3.0-SNAPSHOT + 0.3.3-SNAPSHOT SciJava Scripting: Scala JSR-223-compliant Scala scripting language plugin. @@ -95,16 +95,40 @@ sign,deploy-to-scijava - 3.2.2 + 3.3.0 + 4.8.1 + + + + org.scijava + scijava-common + + + + + org.scala-lang + scala3-compiler_3 + ${scala.version} + + + - - - - net.alchim31.maven - scala-maven-plugin - 4.8.0 + + + junit + junit + test + + + + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin.version} -unchecked @@ -132,31 +156,7 @@ - - - - - - - - org.scijava - scijava-common - - - - - org.scala-lang - scala3-compiler_3 - ${scala.version} - - - - - - - junit - junit - test - - + + + diff --git a/src/main/java/org/scijava/plugins/scripting/scala/Main.java b/src/main/java/org/scijava/plugins/scripting/scala/Main.java index 2d729cc..e272aa8 100644 --- a/src/main/java/org/scijava/plugins/scripting/scala/Main.java +++ b/src/main/java/org/scijava/plugins/scripting/scala/Main.java @@ -2,7 +2,7 @@ * #%L * JSR-223-compliant Scala scripting language plugin. * %% - * Copyright (C) 2013 - 2023 SciJava developers. + * Copyright (C) 2013 - 2024 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java b/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java index 035cbed..1d863e8 100644 --- a/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java +++ b/src/main/java/org/scijava/plugins/scripting/scala/ScalaScriptLanguage.java @@ -2,7 +2,7 @@ * #%L * JSR-223-compliant Scala scripting language plugin. * %% - * Copyright (C) 2013 - 2023 SciJava developers. + * Copyright (C) 2013 - 2024 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,17 +29,17 @@ package org.scijava.plugins.scripting.scala; -import javax.script.ScriptEngine; - import org.scijava.log.LogService; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; import org.scijava.script.AdaptedScriptLanguage; import org.scijava.script.ScriptLanguage; +import javax.script.ScriptEngine; + /** * An adapter of the Scala interpreter to the SciJava scripting interface. - * + * * @author Curtis Rueden * @author Keith Schulze * @author Johannes Schindelin @@ -49,16 +49,10 @@ @Plugin(type = ScriptLanguage.class, name = "Scala") public class ScalaScriptLanguage extends AdaptedScriptLanguage { - @Parameter - private LogService log; - - public ScalaScriptLanguage() { - super("scala"); - } + @Parameter + private LogService log; - @Override - public ScriptEngine getScriptEngine() { - final ScriptEngine eng = new dotty.tools.repl.ScriptEngine(); - return new ScalaAdaptedScriptEngine(eng); - } + public ScalaScriptLanguage() { + super(new ScalaAdaptedScriptEngineFactory()); + } } diff --git a/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory b/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 0000000..45807c2 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1 @@ +org.scijava.plugins.scripting.scala.ScalaScriptLanguage diff --git a/src/main/scala/dotty/tools/repl/SciJavaScriptEngine.scala b/src/main/scala/dotty/tools/repl/SciJavaScriptEngine.scala new file mode 100644 index 0000000..429639e --- /dev/null +++ b/src/main/scala/dotty/tools/repl/SciJavaScriptEngine.scala @@ -0,0 +1,104 @@ +package dotty.tools.repl + +// This file is based on dotty.tools.repl.ScriptEngine in Scala Compiler +// It is using some package private classes from `dotty.tools.repl` +// Only necessary changes are made, no corrects existing compiler warnings, etc. + +import dotty.tools.dotc +import dotty.tools.dotc.core.StdNames.str + +import java.io.{Reader, StringWriter} +import javax.script.{AbstractScriptEngine, Bindings, ScriptContext, ScriptEngineFactory, ScriptException, SimpleBindings, ScriptEngine as JScriptEngine} +import scala.language.unsafeNulls + +/** + * A JSR 223 (Scripting API) compatible wrapper around the REPL for improved + * interoperability with software that supports it. + * + * It works by instantiating a new script engine through the script engine manager. + * The script engine provides a eval method to evaluate scripts in string form. + * Example use: + * + * val m = new javax.script.ScriptEngineManager() + * val e = m.getEngineByName("scala") + * println(e.eval("42")) + */ +class SciJavaScriptEngine(classPath: String) extends AbstractScriptEngine { + new javax.script.ScriptEngineManager() + private val driver = new ReplDriver( + Array( + "-classpath", + classPath, + "-usejavacp", + "-color:never", + "-Xrepl-disable-display" + ), + Console.out, + None + ) + private val rendering = new Rendering(Some(getClass.getClassLoader)) + private var state: State = driver.initialState + + def getFactory: ScriptEngineFactory = new ScriptEngine.Factory + + def createBindings: Bindings = new SimpleBindings + + /* Evaluate with the given context. */ + @throws[ScriptException] + def eval(script: String, context: ScriptContext): Object = { + val vid = state.valIndex + state = driver.run(script)(using state) + val oid = state.objectIndex + Class.forName(s"${Rendering.REPL_WRAPPER_NAME_PREFIX}$oid", true, rendering.classLoader()(using state.context)) + .getDeclaredMethods.find(_.getName == s"${str.REPL_RES_PREFIX}$vid") + .map(_.invoke(null)) + .getOrElse(null) + } + + @throws[ScriptException] + def eval(reader: Reader, context: ScriptContext): Object = eval(stringFromReader(reader), context) + + private val buffer = new Array[Char](8192) + + def stringFromReader(in: Reader) = { + val out = new StringWriter + var n = in.read(buffer) + while (n > -1) { + out.write(buffer, 0, n) + n = in.read(buffer) + } + in.close + out.toString + } +} + +object SciJavaScriptEngine { + import java.util.Arrays + import scala.util.Properties + class Factory(classPath: String) extends ScriptEngineFactory { + def getEngineName = "Scala REPL for SciJava" + def getEngineVersion = "3.0" + def getExtensions = Arrays.asList("scala", "sc") + def getLanguageName = "Scala" + def getLanguageVersion = Properties.versionString + def getMimeTypes = Arrays.asList("application/x-scala") + def getNames = Arrays.asList("scala") + + def getMethodCallSyntax(obj: String, m: String, args: String*) = s"$obj.$m(${args.mkString(", ")})" + + def getOutputStatement(toDisplay: String) = s"""print("$toDisplay")""" + + def getParameter(key: String): Object = key match { + case JScriptEngine.ENGINE => getEngineName + case JScriptEngine.ENGINE_VERSION => getEngineVersion + case JScriptEngine.LANGUAGE => getLanguageName + case JScriptEngine.LANGUAGE_VERSION => getLanguageVersion + case JScriptEngine.NAME => getNames.get(0) + case _ => null + } + + def getProgram(statements: String*) = statements.mkString("; ") + + def getScriptEngine: JScriptEngine = new SciJavaScriptEngine(classPath) + } +} diff --git a/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala b/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala index 8f39b4e..4854b4a 100644 --- a/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala +++ b/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngine.scala @@ -1,3 +1,32 @@ +/* + * #%L + * JSR-223-compliant Scala scripting language plugin. + * %% + * Copyright (C) 2013 - 2023 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + package org.scijava.plugins.scripting.scala import java.io.{OutputStream, Reader, StringWriter, Writer} @@ -5,6 +34,8 @@ import javax.script.* import scala.collection.mutable import scala.jdk.CollectionConverters.* import scala.util.Try +import scala.util.control.NonFatal +import scala.util.matching.Regex /** * Adapted Scala ScriptEngine @@ -37,55 +68,88 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin // Scala 3.2.2 ignores bindings, emulate binding using setup script // Create a line with variable declaration for each binding item - val lines = + val transfers: mutable.Seq[Seq[String]] = for scope <- context.getScopes.asScala bindings <- Option(context.getBindings(scope)).map(_.asScala) // bindings in context can be null yield { - for (name, value) <- bindings yield { - value match - case v: Double => s"val $name : Double = ${v}d" - case v: Float => s"val $name : Float = ${v}f" - case v: Long => s"val $name : Long = ${v}L" - case v: Int => s"val $name : Int = $v" - case v: Char => s"val $name : Char = '$v'" - case v: Short => s"val $name : Short = $v" - case v: Byte => s"val $name : Byte = $v" - case v: Boolean => s"val $name : Int = $v" - case o: AnyRef if isValidVariableName(name) => - _transfer = o - val typeName = Option(o).map(_.getClass.getCanonicalName).getOrElse("AnyRef") - s""" - |val $name : $typeName = { - | val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine._transfer - | t.asInstanceOf[$typeName] - |}""".stripMargin - case _: AnyRef => "" // ignore if name is not a variable - case v: Unit => - throw ScriptException(s"Unsupported type for bind variable $name: ${v.getClass}") + for (name, value) <- bindings.toSeq yield { + if isValidVariableName(name) then + val validName = addBackticksIfNeeded(name) + value match + case v: Double => s"val $validName : Double = ${v}d" + case v: Float => s"val $validName : Float = ${v}f" + case v: Long => s"val $validName : Long = ${v}L" + case v: Int => s"val $validName : Int = $v" + case v: Char => s"val $validName : Char = '$v'" + case v: Short => s"val $validName : Short = $v" + case v: Byte => s"val $validName : Byte = $v" + case v: Boolean => s"val $validName : Int = $v" + case v: AnyRef => + val transferIndex = BindingSupport.nextTransferIndex + BindingSupport.__transfer(transferIndex) = v + val typeName = Option(v) + .map { oo => + val tt: Array[_] = oo.getClass.getTypeParameters + tt.foreach(t => log(s"${oo.getClass.getCanonicalName} TYPE PARAM: ${t.getClass.getName}")) + val p = tt.map(_ => "_").mkString("[", ",", "]") + val n = oo.getClass.getCanonicalName + if tt.nonEmpty then n + p else n + } + .getOrElse("AnyRef") + val validTypeName = addBackticksIfNeeded(typeName) + s""" + |val $validName : $validTypeName = { + | val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine.BindingSupport + | ._transfer($transferIndex) + | t.asInstanceOf[$validTypeName] + |}""".stripMargin + case v: Unit => + throw ScriptException(s"Unsupported type for bind variable $name: ${v.getClass}") + else + "" // ignore if name is not a variable } } - val script = lines + val script = transfers .flatten .filter(_.nonEmpty) .mkString("\n") - if script.nonEmpty then - evalInner(script, context) + evalInner(script, context) end emulateBinding - private def evalInner(script: String, context: ScriptContext) = - class WriterOutputStream(w: Writer) extends OutputStream: - override def write(b: Int): Unit = w.write(b) + private def evalInner(script: String, context: ScriptContext): AnyRef = + log( + s""" + |LOG[evalInner] script + |BEGIN + |--------------------------- + |$script + |--------------------------- + |END + |""".stripMargin + ) + if script.trim.isEmpty then + log("LOG[evalInner] script is empty, skipping evaluation") + null + else + class WriterOutputStream(w: Writer) extends OutputStream: + override def write(b: Int): Unit = w.write(b) - // Redirect output to writes provided by context - Console.withOut(WriterOutputStream(context.getWriter)) { - Console.withErr(WriterOutputStream(context.getErrorWriter)) { - engine.eval(script, context) - } - } + try + // Redirect output to writes provided by context + Console.withOut(WriterOutputStream(context.getWriter)) { + Console.withErr(WriterOutputStream(context.getErrorWriter)) { + engine.eval(script, context) + } + } + catch + case NonFatal(t) => + log(s"LOG[evalInner] in eval: $t") + t.printStackTrace() + throw t private def stringFromReader(in: Reader) = val out = new StringWriter() @@ -122,14 +186,94 @@ class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngin value end get + private def log(msg: String): Unit = { + if ScalaAdaptedScriptEngine.DEBUG then + Console.out.println(msg) + } + end ScalaAdaptedScriptEngine object ScalaAdaptedScriptEngine: + private val DEBUG: Boolean = false private lazy val variableNamePattern = """^[a-zA-Z_$][a-zA-Z_$0-9]*$""".r - - /** Do not use externally despite it is declared public. IT is public so it is accessible from scripts */ - // noinspection ScalaWeakerAccess - var _transfer: Object = _ + private val scala3Keywords = Seq( + "abstract", + "case", + "catch", + "class", + "def", + "do", + "else", + "enum", + "export", + "extends", + "false", + "final", + "finally", + "for", + "given", + "if", + "implicit", + "import", + "lazy", + "match", + "new", + "null", + "object", + "override", + "package", + "private", + "protected", + "return", + "sealed", + "super", + "then", + "throw", + "trait", + "true", + "try", + "type", + "val", + "var", + "while", + "with", + "yield" + ) private def isValidVariableName(name: String): Boolean = variableNamePattern.matches(name) + + private[scala] def addBackticksIfNeeded(referenceName: String): String = + referenceName + .split("\\.") + .map(n => if scala3Keywords.contains(n) then s"`$n`" else n) + .mkString(".") + + /** + * Temporary support for implementing binding in the script engine. + * It has limited capacity and does not free memory. + * Access to storage is public, so it is visible from scripts. + */ + //noinspection ScalaWeakerAccess + object BindingSupport: + private val MaxTransfers: Int = 1024 * 1024 + + /** + * Do not use externally despite it is declared public. + * It is public so it is accessible from scripts that are used to emulate variable binding + */ + // noinspection ScalaWeakerAccess,ScalaUnusedSymbol + def _transfer: Seq[AnyRef] = __transfer.toSeq + + private[scala] val __transfer: mutable.ListBuffer[AnyRef] = mutable.ListBuffer.empty[AnyRef] + + private var lastTransferIndex = -1 + + private[scala] def nextTransferIndex: Int = + if lastTransferIndex + 1 >= MaxTransfers then + throw new IllegalStateException("ScalaAdaptedScriptEngine: maximum transfer limit reached") + + lastTransferIndex += 1 + __transfer.append(null) + lastTransferIndex + end ScalaAdaptedScriptEngine diff --git a/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngineFactory.scala b/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngineFactory.scala new file mode 100644 index 0000000..967baf9 --- /dev/null +++ b/src/main/scala/org/scijava/plugins/scripting/scala/ScalaAdaptedScriptEngineFactory.scala @@ -0,0 +1,79 @@ +/* + * #%L + * JSR-223-compliant Scala scripting language plugin. + * %% + * Copyright (C) 2013 - 2023 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.plugins.scripting.scala + +import org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine + +import java.net.URLClassLoader +import java.nio.file.Paths +import java.util +import javax.script.{ScriptEngine, ScriptEngineFactory} + +/** + * A factory for ScalaAdaptedScriptEngine. + * + * @author Jarek Sacha + * @see ScriptEngineFactory + */ +class ScalaAdaptedScriptEngineFactory extends ScriptEngineFactory: + + private val factory = new dotty.tools.repl.SciJavaScriptEngine.Factory(classPath) + + /** + * Returns an instance of the `ScalaAdaptedScriptEngine`. + * A new instance is returned. + * + * @return A new `ScalaAdaptedScriptEngine` instance. + */ + override def getScriptEngine = new ScalaAdaptedScriptEngine(factory.getScriptEngine) + + override def getEngineName: String = factory.getEngineName + override def getEngineVersion: String = factory.getEngineVersion + override def getExtensions: util.List[String] = factory.getExtensions + override def getLanguageName: String = factory.getLanguageName + override def getLanguageVersion: String = factory.getLanguageVersion + override def getMimeTypes: util.List[String] = factory.getMimeTypes + override def getNames: util.List[String] = factory.getNames + override def getOutputStatement(toDisplay: String): String = factory.getOutputStatement(toDisplay) + override def getParameter(key: String): AnyRef = factory.getParameter(key) + override def getMethodCallSyntax(obj: String, m: String, args: String*): String = + factory.getMethodCallSyntax(obj, m, args*) + override def getProgram(statements: String*): String = factory.getProgram(statements*) + + /** + * Retrieves the current classpath as a string. + */ + def classPath: String = ClassLoader.getSystemClassLoader match + case cl: URLClassLoader => + cl.getURLs + .map(url => Paths.get(url.toURI).toString) + .mkString(System.getProperty("path.separator")) + case _ => + System.getProperty("java.class.path") diff --git a/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java b/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java index fee834e..504bfa2 100644 --- a/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java +++ b/src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java @@ -2,7 +2,7 @@ * #%L * JSR-223-compliant Scala scripting language plugin. * %% - * Copyright (C) 2013 - 2023 SciJava developers. + * Copyright (C) 2013 - 2024 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,12 +35,15 @@ import org.scijava.script.ScriptModule; import org.scijava.script.ScriptService; +import javax.script.ScriptContext; import javax.script.ScriptEngine; +import javax.script.ScriptException; import javax.script.SimpleScriptContext; import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * Scala unit tests. @@ -182,6 +185,28 @@ public void testPutString() throws Exception { } } + @Test + public void testPut3Strings() throws Exception { + // Check that multiple AnyRef variable are bound correctly + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + final String expected1 = "Ala ma kota"; + final String expected2 = "Kot ma Ale"; + final String expected3 = "Reksio nie ma butow"; + engine.put("v1", expected1); + engine.put("v2", expected2); + engine.put("v3", expected3); + final String script = "\n" + + "val o1:String = v1\n" + + "val o2:String = v2\n" + + "val o3:String = v3\n"; + engine.eval(script); + assertEquals(expected1, engine.get("o1")); + assertEquals(expected2, engine.get("o2")); + assertEquals(expected3, engine.get("o3")); + } + } + @Test public void testPutInt() throws Exception { @@ -197,6 +222,23 @@ public void testPutInt() throws Exception { } } + @Test + public void testPutSeqInt() throws Exception { + try (final Context context = new Context(ScriptService.class)) { + final ScriptEngine engine = getEngine(context); + + final List expected = new ArrayList<>(); + expected.add(7); + expected.add(13); + expected.add(-1); + engine.put("v", expected); + final String script = "val v1 = v"; + engine.eval(script); + final Object actual = engine.get("v1"); + assertEquals(expected, actual); + } + } + @Test public void testLocals() throws Exception { @@ -210,7 +252,7 @@ public void testLocals() throws Exception { assertEquals("17", engine.eval("hello").toString()); assertEquals("17", engine.get("hello").toString()); -// With Scala 3.2.2 cannot reset bindings correctly, will skip the ret of the test +// With Scala 3.2.2 cannot reset bindings correctly, will skip the rest of the test // final Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); // bindings.clear(); // assertNull(engine.get("hello")); @@ -266,4 +308,64 @@ public void testImportsRetained() throws Exception { assertEquals(result, result2); } } + + /** + * Test for issue #5: "eval should sometimes make an entry in the ENGINE_SCOPE bindings" + */ + @Test + public void issue5() { + try (final Context context = new Context(ScriptService.class)) { + + final ScriptService scriptService = context.getService(ScriptService.class); + final ScriptEngine engine = scriptService.getLanguageByName("scala").getScriptEngine(); + + assertFalse(engine.getBindings(ScriptContext.ENGINE_SCOPE).containsKey("ten")); + engine.put("ten", 10); + assertEquals(10, engine.get("ten")); + assertTrue(engine.getBindings(ScriptContext.ENGINE_SCOPE).containsKey("ten")); + + engine.put("twenty", 20); + assertEquals(20, engine.get("twenty")); + } + } + + /** + * Test for issue #9: "ScriptREPL is not usable - problem with variables named as Scala keywords" + */ + @Test + public void issue9() { + try (final Context context = new Context(ScriptService.class)) { + + final ScriptService scriptService = context.getService(ScriptService.class); + final ScriptEngine engine = scriptService.getLanguageByName("scala").getScriptEngine(); + + final String name = "object"; + final int expectedValue = 7; + + assertFalse(engine.getBindings(ScriptContext.ENGINE_SCOPE).containsKey(name)); + engine.put(name, expectedValue); + assertTrue(engine.getBindings(ScriptContext.ENGINE_SCOPE).containsKey(name)); + assertEquals(expectedValue, engine.get(name)); + + final String script1 = "val abc = `" + name + "`"; + try { + engine.eval(script1); + } catch (ScriptException e) { + fail("Failed to execute valid script: \"" + script1 + "\""); + } + + final Object r = engine.get("abc"); + assertEquals(expectedValue, r); + + } + } + + @Test + public void addBackticksIfNeededTest() { + final String expected = "org.scijava.`object`.DefaultObjectService"; + final String name = "org.scijava.object.DefaultObjectService"; + final String correctedName = ScalaAdaptedScriptEngine.addBackticksIfNeeded(name); + + assertEquals(expected, correctedName); + } }