From 95bd38e598a4baf7a2389b5a5026e718967a7bb9 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Tue, 2 May 2023 10:44:42 +0200 Subject: [PATCH 1/2] Refactor JDI wrappers --- .../internal/EvaluationProvider.scala | 21 +-- .../internal/evaluator/FrameReference.scala | 8 - .../internal/evaluator/JdiArray.scala | 38 ++--- .../internal/evaluator/JdiClass.scala | 77 ++++++++++ .../internal/evaluator/JdiClassLoader.scala | 131 ++++++++++------ .../internal/evaluator/JdiClassObject.scala | 36 ----- .../internal/evaluator/JdiFrame.scala | 41 +++++ .../internal/evaluator/JdiObject.scala | 46 ++++-- .../internal/evaluator/JdiPrimitive.scala | 26 ---- .../internal/evaluator/JdiString.scala | 7 + .../internal/evaluator/JdiValue.scala | 57 +++++++ .../internal/evaluator/MessageLogger.scala | 15 +- .../evaluator/MethodInvocationFailed.scala | 4 +- .../internal/evaluator/ScalaEvaluator.scala | 142 +++++------------- .../internal/evaluator/SimpleEvaluator.scala | 35 ++--- .../internal/evaluator/package.scala | 37 ----- 16 files changed, 376 insertions(+), 345 deletions(-) delete mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/FrameReference.scala create mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala delete mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassObject.scala create mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala delete mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiPrimitive.scala create mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiString.scala create mode 100644 modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala index 8c10a625e..9f04f7bcd 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala @@ -11,8 +11,9 @@ import ch.epfl.scala.debugadapter.Logger import ch.epfl.scala.debugadapter.ManagedEntry import ch.epfl.scala.debugadapter.UnmanagedEntry import ch.epfl.scala.debugadapter.internal.evaluator.CompiledExpression -import ch.epfl.scala.debugadapter.internal.evaluator.FrameReference +import ch.epfl.scala.debugadapter.internal.evaluator.JdiFrame import ch.epfl.scala.debugadapter.internal.evaluator.JdiObject +import ch.epfl.scala.debugadapter.internal.evaluator.JdiValue import ch.epfl.scala.debugadapter.internal.evaluator.LocalValue import ch.epfl.scala.debugadapter.internal.evaluator.MessageLogger import ch.epfl.scala.debugadapter.internal.evaluator.MethodInvocationFailed @@ -53,7 +54,7 @@ private[internal] class EvaluationProvider( override def isInEvaluation(thread: ThreadReference) = isEvaluating.get override def evaluate(expression: String, thread: ThreadReference, depth: Int): CompletableFuture[Value] = { - val frame = FrameReference(thread, depth) + val frame = JdiFrame(thread, depth) val evaluation = for { preparedExpression <- prepare(expression, frame) evaluation <- evaluate(preparedExpression, frame) @@ -71,7 +72,7 @@ private[internal] class EvaluationProvider( breakpoint: IEvaluatableBreakpoint, thread: ThreadReference ): CompletableFuture[Value] = { - val frame = FrameReference(thread, 0) + val frame = JdiFrame(thread, 0) val location = frame.current().location val locationCode = (location.method.name, location.codeIndex).hashCode val expression = @@ -96,18 +97,20 @@ private[internal] class EvaluationProvider( thisContext: ObjectReference, methodName: String, methodSignature: String, - args: Array[Value], + rawArgs: Array[Value], thread: ThreadReference, invokeSuper: Boolean ): CompletableFuture[Value] = { - val obj = new JdiObject(thisContext, thread) + val obj = JdiObject(thisContext, thread) + val args = if (rawArgs == null) Seq.empty else rawArgs.toSeq.map(JdiValue(_, thread)) val invocation = evaluationBlock { obj - .invoke(methodName, methodSignature, if (args == null) List() else args.toList) + .invoke(methodName, methodSignature, args) .recover { // if invocation throws an exception, we return that exception as the result case MethodInvocationFailed(msg, exception) => exception } + .map(_.value) } completeFuture(invocation.getResult, thread) } @@ -132,7 +135,7 @@ private[internal] class EvaluationProvider( case _ => s"Unsupported evaluation in ${entry.name}" } - private def prepareLogMessage(message: String, frame: FrameReference): Try[PreparedExpression] = { + private def prepareLogMessage(message: String, frame: JdiFrame): Try[PreparedExpression] = { if (!message.contains("$")) { Success(PlainLogMessage(message)) } else { @@ -142,7 +145,7 @@ private[internal] class EvaluationProvider( } } - private def prepare(expression: String, frame: FrameReference): Try[PreparedExpression] = { + private def prepare(expression: String, frame: JdiFrame): Try[PreparedExpression] = { lazy val simpleExpression = simpleEvaluator.prepare(expression, frame) if (mode.allowSimpleEvaluation && simpleExpression.isDefined) { Success(simpleExpression.get) @@ -160,7 +163,7 @@ private[internal] class EvaluationProvider( } } - private def evaluate(expression: PreparedExpression, frame: FrameReference): Try[Value] = { + private def evaluate(expression: PreparedExpression, frame: JdiFrame): Try[Value] = { expression match { case logMessage: PlainLogMessage => messageLogger.log(logMessage, frame) case localValue: LocalValue => simpleEvaluator.evaluate(localValue, frame) diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/FrameReference.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/FrameReference.scala deleted file mode 100644 index 408052e20..000000000 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/FrameReference.scala +++ /dev/null @@ -1,8 +0,0 @@ -package ch.epfl.scala.debugadapter.internal.evaluator - -import com.sun.jdi.ThreadReference -import com.sun.jdi.StackFrame - -final case class FrameReference(thread: ThreadReference, depth: Int) { - def current(): StackFrame = thread.frame(depth) -} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala index e4d2e871b..41eda6ed8 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala @@ -1,39 +1,21 @@ package ch.epfl.scala.debugadapter.internal.evaluator -import com.sun.jdi.{ArrayReference, ThreadReference, Value} +import com.sun.jdi.ArrayReference +import com.sun.jdi.ThreadReference +import com.sun.jdi.Value import scala.jdk.CollectionConverters.* -object JdiArray { - def apply(value: Value, thread: ThreadReference): JdiArray = - new JdiArray(value.asInstanceOf[ArrayReference], thread) - - def apply(arrayType: String, arraySize: Int, classLoader: JdiClassLoader): Safe[JdiArray] = { - val thread = classLoader.thread - for { - classClass <- classLoader.loadClass("java.lang.Class") - intValue <- classLoader.mirrorOf("int") - intClass <- - classClass.invoke("getPrimitiveClass", List(intValue)) - arrayClass <- classLoader.loadClass("java.lang.reflect.Array") - newInstanceValue <- classLoader.mirrorOf("newInstance") - newInstanceMethod <- arrayClass - .invoke("getMethod", List(newInstanceValue, classClass.reference, intClass)) - .map(JdiObject(_, thread)) - arrayTypeClass <- classLoader.loadClass(arrayType) - integerRef <- JdiPrimitive.box(arraySize, classLoader, thread) - array <- newInstanceMethod - .invoke("invoke", List(null, arrayTypeClass.reference, integerRef)) - .map(JdiArray(_, thread)) - } yield array - } -} - class JdiArray(reference: ArrayReference, thread: ThreadReference) extends JdiObject(reference, thread) { def setValue(index: Int, value: Value): Unit = reference.setValue(index, value) - def setValues(values: Seq[Value]): Unit = reference.setValues(values.asJava) + def setValues(values: Seq[JdiValue]): Unit = reference.setValues(values.map(_.value).asJava) - def getValues: Seq[Value] = reference.getValues.asScala.toSeq + def getValues: Seq[JdiValue] = reference.getValues.asScala.toSeq.map(JdiValue(_, thread)) +} + +object JdiArray { + def apply(value: Value, thread: ThreadReference): JdiArray = + new JdiArray(value.asInstanceOf[ArrayReference], thread) } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala new file mode 100644 index 000000000..73da5f660 --- /dev/null +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala @@ -0,0 +1,77 @@ +package ch.epfl.scala.debugadapter.internal.evaluator + +import com.sun.jdi._ + +import scala.collection.JavaConverters.* +import scala.util.control.NonFatal + +private[internal] class JdiClass( + cls: ClassType, + thread: ThreadReference +) extends JdiObject(cls.classObject, thread) { + + def initialized: Boolean = cls.isInitialized + def className: String = cls.name + override def classLoader: JdiClassLoader = JdiClassLoader(cls.classLoader, thread) + + def newInstance(args: Seq[JdiValue]): Safe[JdiObject] = { + val ctr = cls.methodsByName("").asScala.head + newInstance(ctr, args) + } + + def newInstance(signature: String, args: Seq[JdiValue]): Safe[JdiObject] = { + val ctr = cls.methodsByName("", signature).asScala.head + newInstance(ctr, args) + } + + private def newInstance(ctr: Method, args: Seq[JdiValue]): Safe[JdiObject] = + for { + _ <- prepareMethod(ctr) + instance <- Safe(cls.newInstance(thread, ctr, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED)) + .recoverWith(recoverInvocationException(thread)) + } yield JdiObject(instance, thread) + + // Load the argument types of the method to avoid ClassNotLoadedException + // TODO Should we use this method before all invocations: methods, ctrs, fields + private def prepareMethod(method: Method): Safe[Unit] = { + def loadArgumentsRecursively(): Safe[Unit] = { + try { + method.argumentTypes() + Safe(()) + } catch { + case exception: ClassNotLoadedException => + val className = exception.className.stripSuffix("[]").replace('/', '.') + classLoader.loadClass(className).flatMap(_ => loadArgumentsRecursively()) + case NonFatal(cause) => Safe(throw cause) + } + } + loadArgumentsRecursively() + } + + def getStaticField(fieldName: String): Safe[JdiValue] = + Safe(cls.getValue(cls.fieldByName(fieldName))).map(JdiValue(_, thread)) + + def invokeStatic(methodName: String, args: Seq[JdiValue]): Safe[JdiValue] = { + cls.methodsByName(methodName).forEach(m => println(m.signature)) + val method = cls.methodsByName(methodName).asScala.head + invokeStatic(method, args) + } + + def invokeStatic(methodName: String, signature: String, args: Seq[JdiValue]): Safe[JdiValue] = { + val method = cls.methodsByName(methodName, signature).asScala.head + invokeStatic(method, args) + } + + protected def invokeStatic(method: Method, args: Seq[JdiValue]): Safe[JdiValue] = + Safe(cls.invokeMethod(thread, method, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED)) + .map(JdiValue(_, thread)) + .recoverWith(recoverInvocationException(thread)) +} + +object JdiClass { + def apply(classType: ReferenceType, thread: ThreadReference): JdiClass = + new JdiClass(classType.asInstanceOf[ClassType], thread) + + def apply(classObject: ClassObjectReference, thread: ThreadReference): JdiClass = + new JdiClass(classObject.reflectedType.asInstanceOf[ClassType], thread) +} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala index 79552161a..e54ca537c 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala @@ -1,62 +1,95 @@ package ch.epfl.scala.debugadapter.internal.evaluator import com.sun.jdi._ -import scala.collection.JavaConverters.* -private[internal] object JdiClassLoader { - def fromFrame(frame: FrameReference): Safe[JdiClassLoader] = Safe { - val scalaLibClassLoader = - for { - scalaLibClass <- frame.thread.virtualMachine.allClasses.asScala - .find(c => c.name.startsWith("scala.runtime")) - classLoader <- Option(scalaLibClass.classLoader) - } yield classLoader - - val classLoader = Option(frame.current().location.method.declaringType.classLoader) - .orElse(scalaLibClassLoader) - .getOrElse(throw new Exception("Cannot find the classloader of the Scala library")) - JdiClassLoader(classLoader, frame.thread) - } - - def apply( - classLoader: ClassLoaderReference, - thread: ThreadReference - ): JdiClassLoader = { - val classLoaderType = classLoader.referenceType - val loadClassMethod = method( - "loadClass", - "(Ljava/lang/String;)Ljava/lang/Class;", - classLoaderType - ) - new JdiClassLoader(classLoader, loadClassMethod, thread) - } -} +import java.nio.file.Path -private[internal] case class JdiClassLoader( - classLoaderRef: ClassLoaderReference, - loadClassMethod: Method, +private[internal] class JdiClassLoader( + reference: ClassLoaderReference, thread: ThreadReference -) { +) extends JdiObject(reference, thread) { - def loadClass(name: String): Safe[JdiClassObject] = { + private def loadClassClass: Safe[JdiClass] = for { - nameValue <- mirrorOf(name) - classObject <- invokeMethod( - classLoaderRef, - loadClassMethod, - List(nameValue), - thread + classClassName <- mirrorOf("java.lang.Class") + classClass <- invoke("loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", Seq(classClassName)) + } yield classClass.asClass + + def loadClass(className: String): Safe[JdiClass] = + for { + classNameValue <- mirrorOf(className) + classClass <- loadClassClass + classObject <- classClass.invokeStatic( + "forName", + "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;", + Seq(classNameValue, mirrorOf(true), this) ) + } yield classObject.asClass + + def mirrorOf(str: String): Safe[JdiString] = + Safe(thread.virtualMachine.mirrorOf(str)).map(new JdiString(_, thread)) + + def mirrorOf(boolean: Boolean): JdiValue = + new JdiValue(thread.virtualMachine.mirrorOf(boolean), thread) + + def mirrorOf(integer: Int): JdiValue = + new JdiValue(thread.virtualMachine.mirrorOf(integer), thread) + + def boxIfPrimitive(value: JdiValue): Safe[JdiValue] = + value.value match { + case value: BooleanValue => box(value.value) + case value: CharValue => box(value.value) + case value: DoubleValue => box(value.value) + case value: FloatValue => box(value.value) + case value: IntegerValue => box(value.value) + case value: LongValue => box(value.value) + case value: ShortValue => box(value.value) + case value => Safe(JdiValue(value, thread)) + } + + def box(value: AnyVal): Safe[JdiObject] = + for { + jdiValue <- mirrorOf(value.toString) + _ = getClass + (className, sig) = value match { + case _: Boolean => ("java.lang.Boolean", "(Ljava/lang/String;)Ljava/lang/Boolean;") + case _: Byte => ("java.lang.Byte", "(Ljava/lang/String;)Ljava/lang/Byte;") + case _: Char => ("java.lang.Character", "(Ljava/lang/String;)Ljava/lang/Character;") + case _: Double => ("java.lang.Double", "(Ljava/lang/String;)Ljava/lang/Double;") + case _: Float => ("java.lang.Float", "(Ljava/lang/String;)Ljava/lang/Float;") + case _: Int => ("java.lang.Integer", "(Ljava/lang/String;)Ljava/lang/Integer;") + case _: Long => ("java.lang.Long", "(Ljava/lang/String;)Ljava/lang/Long;") + case _: Short => ("java.lang.Short", "(Ljava/lang/String;)Ljava/lang/Short;") + } + clazz <- loadClass(className) + objectRef <- clazz.invokeStatic("valueOf", sig, List(jdiValue)) + } yield objectRef.asObject + + def createArray(arrayType: String, values: Seq[JdiValue]): Safe[JdiArray] = + for { + arrayTypeClass <- loadClass(arrayType) + arrayClass <- loadClass("java.lang.reflect.Array") + size = mirrorOf(values.size) + array <- arrayClass + .invokeStatic("newInstance", "(Ljava/lang/Class;I)Ljava/lang/Object;", Seq(arrayTypeClass, size)) + .map(_.asArray) } yield { - new JdiClassObject( - classObject.asInstanceOf[ClassObjectReference], - this, - thread - ) + array.setValues(values) + array } - } - def mirrorOf(str: String): Safe[StringReference] = { - Safe(thread.virtualMachine.mirrorOf(str)) - } + def createChildLoader(classPathEntry: Path): Safe[JdiClassLoader] = + for { + classPathValue <- mirrorOf(classPathEntry.toUri.toString) + urlClass <- loadClass("java.net.URL") + url <- urlClass.newInstance("(Ljava/lang/String;)V", List(classPathValue)) + urls <- createArray("java.net.URL", Seq(url)) + classOfUrlClassLoader <- loadClass("java.net.URLClassLoader") + urlClassLoader <- classOfUrlClassLoader.newInstance("([Ljava/net/URL;Ljava/lang/ClassLoader;)V", List(urls, this)) + } yield urlClassLoader.asClassLoader +} + +private[internal] object JdiClassLoader { + def apply(ref: ClassLoaderReference, thread: ThreadReference): JdiClassLoader = + new JdiClassLoader(ref, thread) } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassObject.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassObject.scala deleted file mode 100644 index b330292b1..000000000 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassObject.scala +++ /dev/null @@ -1,36 +0,0 @@ -package ch.epfl.scala.debugadapter.internal.evaluator - -import com.sun.jdi._ - -class JdiClassObject( - reference: ClassObjectReference, - classLoader: JdiClassLoader, - thread: ThreadReference -) extends JdiObject(reference, thread) { - - def newInstance(args: List[ObjectReference]): Safe[JdiObject] = { - val parameterTypes = args.map(_.referenceType.classObject()) - for { - objects <- JdiArray("java.lang.Object", args.size, classLoader) - _ = objects.setValues(args) - constructor <- invoke("getConstructor", parameterTypes) - .map(JdiObject(_, thread)) - jdiObject <- constructor - .invoke("newInstance", List(objects.reference)) - .map(JdiObject(_, thread)) - } yield jdiObject - } - - def invokeStatic( - methodName: String, - args: List[ObjectReference] - ): Safe[Value] = { - val parameterTypes = args.map(_.referenceType.classObject()) - for { - methodNameValue <- classLoader.mirrorOf(methodName) - method <- invoke("getMethod", methodNameValue :: parameterTypes) - .map(JdiObject(_, thread)) - result <- method.invoke("invoke", List(null) ++ args) - } yield result - } -} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala new file mode 100644 index 000000000..740ea7f17 --- /dev/null +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala @@ -0,0 +1,41 @@ +package ch.epfl.scala.debugadapter.internal.evaluator + +import com.sun.jdi.ClassLoaderReference +import com.sun.jdi.LocalVariable +import com.sun.jdi.StackFrame +import com.sun.jdi.ThreadReference + +import scala.collection.JavaConverters._ + +final case class JdiFrame(thread: ThreadReference, depth: Int) { + def current(): StackFrame = thread.frame(depth) + + // it's a Safe because it can fail, but it could also be a Try + def classLoader(): Safe[JdiClassLoader] = Safe { + def getClassLoaderRecursively(depth: Int): Option[ClassLoaderReference] = + if (depth == thread.frameCount) None + else { + Option(thread.frame(depth).location.method.declaringType.classLoader) + .orElse(getClassLoaderRecursively(depth + 1)) + } + + val classLoader = getClassLoaderRecursively(depth) + .getOrElse(throw new Exception("Cannot find any classloader in the stack trace")) + JdiClassLoader(classLoader, thread) + } + + // this object can be null + def thisObject(): Option[JdiObject] = + Option(current().thisObject).map(JdiObject(_, thread)) + + def variables(): Seq[LocalVariable] = + current().visibleVariables.asScala.toSeq + + def variablesAndValues(): Seq[(LocalVariable, JdiValue)] = { + val frame = current() + variables().map(v => v -> JdiValue(frame.getValue(v), thread)) + } + + def setVariable(variable: LocalVariable, value: JdiValue): Unit = + current().setValue(variable, value.value) +} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala index 6c4226c1a..b11853a05 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala @@ -2,24 +2,52 @@ package ch.epfl.scala.debugadapter.internal.evaluator import com.sun.jdi._ +import scala.collection.JavaConverters.* + private[internal] class JdiObject( val reference: ObjectReference, thread: ThreadReference -) { - def getFieldValue(name: String): Value = { +) extends JdiValue(reference, thread) { + def getField(name: String): JdiValue = { val field = reference.referenceType.fieldByName(name) - reference.getValue(field) + JdiValue(reference.getValue(field), thread) + } + + def invoke(methodName: String, args: Seq[JdiValue]): Safe[JdiValue] = { + val m = reference.referenceType.methodsByName(methodName).asScala.head + invoke(m, args) + } + + def invoke(methodName: String, signature: String, args: Seq[JdiValue]): Safe[JdiValue] = { + val m = reference.referenceType.methodsByName(methodName, signature).asScala.head + invoke(m, args) } - def invoke(methodName: String, args: List[Value]): Safe[Value] = { - val m = method(methodName, reference.referenceType) - invokeMethod(reference, m, args, thread) + def classObject: JdiClass = JdiClass(reference.referenceType, thread) + def classLoader: JdiClassLoader = JdiClassLoader(reference.referenceType.classLoader, thread) + + protected def invoke(method: Method, args: Seq[JdiValue]): Safe[JdiValue] = { + Safe(reference.invokeMethod(thread, method, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED)) + .map(JdiValue(_, thread)) + .recoverWith(recoverInvocationException(thread)) } - def invoke(methodName: String, signature: String, args: List[Value]): Safe[Value] = { - val m = method(methodName, signature, reference.referenceType()) - invokeMethod(reference, m, args, thread) + protected def recoverInvocationException(thread: ThreadReference): PartialFunction[Throwable, Safe[Nothing]] = { + case t: InvocationException => + extractMessage(t, thread).map { message => + throw new MethodInvocationFailed(message, JdiObject(t.exception, thread)) + } } + + private def extractMessage(invocationException: InvocationException, thread: ThreadReference): Safe[String] = + JdiObject(invocationException.exception(), thread) + .invoke("toString", List()) + .map(_.asString.stringValue) + .recover { case _ => "" } + + def fields: Seq[(String, JdiValue)] = + reference.referenceType.fields.asScala.toSeq + .map(f => (f.name, JdiValue(reference.getValue(f), thread))) } private[internal] object JdiObject { diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiPrimitive.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiPrimitive.scala deleted file mode 100644 index 5227cdf05..000000000 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiPrimitive.scala +++ /dev/null @@ -1,26 +0,0 @@ -package ch.epfl.scala.debugadapter.internal.evaluator - -import com.sun.jdi.{ObjectReference, ThreadReference} - -object JdiPrimitive { - def box( - value: AnyVal, - classLoader: JdiClassLoader, - thread: ThreadReference - ): Safe[ObjectReference] = { - for { - jdiValue <- classLoader.mirrorOf(value.toString) - clazz <- value match { - case _: Boolean => classLoader.loadClass("java.lang.Boolean") - case _: Byte => classLoader.loadClass("java.lang.Byte") - case _: Char => classLoader.loadClass("java.lang.Character") - case _: Double => classLoader.loadClass("java.lang.Double") - case _: Float => classLoader.loadClass("java.lang.Float") - case _: Int => classLoader.loadClass("java.lang.Integer") - case _: Long => classLoader.loadClass("java.lang.Long") - case _: Short => classLoader.loadClass("java.lang.Short") - } - objectRef <- clazz.invokeStatic("valueOf", List(jdiValue)) - } yield objectRef.asInstanceOf[ObjectReference] - } -} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiString.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiString.scala new file mode 100644 index 000000000..17e6c3bd1 --- /dev/null +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiString.scala @@ -0,0 +1,7 @@ +package ch.epfl.scala.debugadapter.internal.evaluator + +import com.sun.jdi._ + +private[internal] class JdiString(ref: StringReference, thread: ThreadReference) extends JdiValue(ref, thread) { + def stringValue: String = ref.value +} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala new file mode 100644 index 000000000..88e8ec6bd --- /dev/null +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala @@ -0,0 +1,57 @@ +package ch.epfl.scala.debugadapter.internal.evaluator + +import com.sun.jdi._ + +private[internal] class JdiValue(val value: Value, val thread: ThreadReference) { + def asObject: JdiObject = JdiObject(value.asInstanceOf[ObjectReference], thread) + def asClass: JdiClass = JdiClass(value.asInstanceOf[ClassObjectReference], thread) + def asClassLoader: JdiClassLoader = JdiClassLoader(value.asInstanceOf[ClassLoaderReference], thread) + def asArray: JdiArray = JdiArray(value.asInstanceOf[ArrayReference], thread) + def asString: JdiString = new JdiString(value.asInstanceOf[StringReference], thread) + + def unboxIfPrimitive: Safe[JdiValue] = { + value match { + case ref: ObjectReference => + val typeName = ref.referenceType.name + JdiValue.unboxMethods + .get(typeName) + .map(methodName => asObject.invoke(methodName, Nil)) + .getOrElse(Safe(this)) + case _ => Safe(this) + } + } + + def derefIfRef: JdiValue = + value match { + case ref: ObjectReference if JdiValue.refTypes.contains(ref.referenceType.name) => + asObject.getField("elem") + case _ => this + } +} + +object JdiValue { + def apply(value: Value, thread: ThreadReference): JdiValue = new JdiValue(value, thread) + + private val unboxMethods = Map( + "java.lang.Boolean" -> "booleanValue", + "java.lang.Byte" -> "byteValue", + "java.lang.Character" -> "charValue", + "java.lang.Double" -> "doubleValue", + "java.lang.Float" -> "floatValue", + "java.lang.Integer" -> "intValue", + "java.lang.Long" -> "longValue", + "java.lang.Short" -> "shortValue" + ) + + private val refTypes = Set( + "scala.runtime.BooleanRef", + "scala.runtime.ByteRef", + "scala.runtime.CharRef", + "scala.runtime.DoubleRef", + "scala.runtime.FloatRef", + "scala.runtime.IntRef", + "scala.runtime.LongRef", + "scala.runtime.ShortRef", + "scala.runtime.ObjectRef" + ) +} diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MessageLogger.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MessageLogger.scala index 1aa380414..4542e29d9 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MessageLogger.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MessageLogger.scala @@ -1,21 +1,18 @@ package ch.epfl.scala.debugadapter.internal.evaluator import com.sun.jdi.* + import scala.util.Try private[internal] class MessageLogger() { - def log(logMessage: PlainLogMessage, frame: FrameReference): Try[Value] = { + def log(logMessage: PlainLogMessage, frame: JdiFrame): Try[Value] = { val result = for { - classLoader <- JdiClassLoader.fromFrame(frame) - vm = frame.thread.virtualMachine() - arg <- Safe(vm.mirrorOf(logMessage.message)) + classLoader <- frame.classLoader() + arg <- classLoader.mirrorOf(logMessage.message) predefClass <- classLoader.loadClass("scala.Predef$") - moduleField <- predefClass - .invoke("getDeclaredField", List(vm.mirrorOf("MODULE$"))) - .map(JdiObject(_, frame.thread)) - predef <- moduleField.invoke("get", List(null)).map(JdiObject(_, frame.thread)) + predef <- predefClass.getStaticField("MODULE$").map(_.asObject) res <- predef.invoke("println", "(Ljava/lang/Object;)V", List(arg)) - } yield res + } yield res.value result.getResult } } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MethodInvocationFailed.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MethodInvocationFailed.scala index dabfe4fae..f5c9756e9 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MethodInvocationFailed.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/MethodInvocationFailed.scala @@ -1,8 +1,6 @@ package ch.epfl.scala.debugadapter.internal.evaluator -import com.sun.jdi.ObjectReference - private[internal] case class MethodInvocationFailed( message: String, - remoteException: ObjectReference + remoteException: JdiObject ) extends Exception(message) diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala index c48d81bc8..407cc00dd 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala @@ -1,14 +1,13 @@ package ch.epfl.scala.debugadapter.internal.evaluator +import ch.epfl.scala.debugadapter.ClassEntry import ch.epfl.scala.debugadapter.Logger import com.sun.jdi._ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path -import scala.jdk.CollectionConverters.* import scala.util.Try -import ch.epfl.scala.debugadapter.ClassEntry private[internal] class ScalaEvaluator( entry: ClassEntry, @@ -16,12 +15,12 @@ private[internal] class ScalaEvaluator( logger: Logger, testMode: Boolean ) { - def evaluate(expression: CompiledExpression, frame: FrameReference): Try[Value] = { + def evaluate(expression: CompiledExpression, frame: JdiFrame): Try[Value] = { val CompiledExpression(classDir, className) = expression evaluate(classDir, className, frame) } - def compile(sourceContent: String, expression: String, frame: FrameReference): Try[CompiledExpression] = { + def compile(sourceContent: String, expression: String, frame: JdiFrame): Try[CompiledExpression] = { logger.debug(s"Compiling expression '$expression'") val location = frame.current().location val line = location.lineNumber @@ -40,9 +39,9 @@ private[internal] class ScalaEvaluator( val expressionFqcn = if (packageName.isEmpty) expressionClassName else s"$packageName.$expressionClassName" val compiledExpression = for { - classLoader <- JdiClassLoader.fromFrame(frame) + classLoader <- frame.classLoader() (names, values) <- extractValuesAndNames(frame, classLoader) - localNames = names.map(_.value()).toSet + localNames = names.map(_.stringValue).toSet _ <- Safe( compiler.compile(outDir, expressionClassName, sourceFile, line, expression, localNames, packageName, testMode) ) @@ -50,24 +49,22 @@ private[internal] class ScalaEvaluator( compiledExpression.getResult } - private def evaluate(classDir: Path, className: String, frame: FrameReference): Try[Value] = { + private def evaluate(classDir: Path, className: String, frame: JdiFrame): Try[Value] = { val evaluatedValue = for { - classLoader <- JdiClassLoader.fromFrame(frame) + classLoader <- frame.classLoader() (names, values) <- extractValuesAndNames(frame, classLoader) - namesArray <- JdiArray("java.lang.String", names.size, classLoader) - valuesArray <- JdiArray("java.lang.Object", values.size, classLoader) - _ = namesArray.setValues(names) - _ = valuesArray.setValues(values) - args = List(namesArray.reference, valuesArray.reference) + namesArray <- classLoader.createArray("java.lang.String", names) + valuesArray <- classLoader.createArray("java.lang.Object", values) + args = List(namesArray, valuesArray) expressionInstance <- createExpressionInstance(classLoader, classDir, className, args) evaluatedValue <- evaluateExpression(expressionInstance) _ <- updateVariables(valuesArray, frame) - unboxedValue <- unboxIfPrimitive(evaluatedValue, frame.thread) - } yield unboxedValue + unboxedValue <- evaluatedValue.unboxIfPrimitive + } yield unboxedValue.value evaluatedValue.getResult } - private def evaluateExpression(expressionInstance: JdiObject): Safe[Value] = { + private def evaluateExpression(expressionInstance: JdiObject): Safe[JdiValue] = { expressionInstance .invoke("evaluate", List()) .recover { @@ -85,22 +82,13 @@ private[internal] class ScalaEvaluator( classLoader: JdiClassLoader, classDir: Path, className: String, - args: List[ObjectReference] + args: List[JdiObject] ): Safe[JdiObject] = { val expressionClassPath = classDir.toUri.toString + getClass() for { - classPathValue <- classLoader.mirrorOf(expressionClassPath) - urlClass <- classLoader - .loadClass("java.net.URL") - url <- urlClass.newInstance(List(classPathValue)) - urls <- JdiArray("java.net.URL", 1, classLoader) - _ = urls.setValue(0, url.reference) - urlClassLoader <- classLoader - .loadClass("java.net.URLClassLoader") - .flatMap(_.newInstance(List(urls.reference))) - .map(_.reference.asInstanceOf[ClassLoaderReference]) - .map(JdiClassLoader(_, classLoader.thread)) - expressionClass <- urlClassLoader.loadClass(className) + expressionClassLoader <- classLoader.createChildLoader(classDir) + expressionClass <- expressionClassLoader.loadClass(className) expressionInstance <- expressionClass.newInstance(args) } yield expressionInstance } @@ -113,33 +101,29 @@ private[internal] class ScalaEvaluator( * @return Tuple of extracted names and values */ private def extractValuesAndNames( - frameRef: FrameReference, + frameRef: JdiFrame, classLoader: JdiClassLoader - ): Safe[(Seq[StringReference], Seq[Value])] = { - val frame = frameRef.current() - val thisObjectOpt = Option(frame.thisObject) // this object can be null - def extractVariablesFromFrame(): Safe[(Seq[StringReference], Seq[Value])] = { - val localVariables = frame.visibleVariables().asScala.toSeq.map(v => v.name -> frame.getValue(v)) - val thisObject = thisObjectOpt.filter(_ => !localVariables.exists(_._1 == "$this")).map("$this".->) + ): Safe[(Seq[JdiString], Seq[JdiValue])] = { + def extractVariablesFromFrame(): Safe[(Seq[JdiString], Seq[JdiValue])] = { + val localVariables = frameRef.variablesAndValues().map { case (variable, value) => (variable.name, value) } + val thisObject = frameRef.thisObject().filter(_ => !localVariables.contains("$this")).map("$this".->) (localVariables ++ thisObject) .map { case (name, value) => for { name <- classLoader.mirrorOf(name) - value <- boxIfPrimitive(value, classLoader) + value <- classLoader.boxIfPrimitive(value) } yield (name, value) } + .toSeq .traverse .map(xs => (xs.map(_._1), xs.map(_._2))) } // Only useful in Scala 2 - def extractFields(thisObject: ObjectReference): Safe[(Seq[StringReference], Seq[Value])] = { - val fields = thisObject.referenceType.fields.asScala.toList - val fieldNames = fields.map(_.name).map(classLoader.mirrorOf).traverse - val fieldValues = fields - .map(field => thisObject.getValue(field)) - .map(value => boxIfPrimitive(value, classLoader)) - .traverse - Safe.join(fieldNames, fieldValues) + def extractFields(thisObject: JdiObject): Safe[(Seq[JdiString], Seq[JdiValue])] = { + val fields = thisObject.fields + val names = fields.map(_._1).map(classLoader.mirrorOf).traverse + val values = fields.map(_._2).map(value => classLoader.boxIfPrimitive(value)).traverse + Safe.join(names, values) } for { @@ -148,7 +132,8 @@ private[internal] class ScalaEvaluator( // expression evaluator. // It is dangerous because local values can shadow fields // TODO: adapt Scala 2 expression compiler - (fieldNames, fieldValues) <- thisObjectOpt + (fieldNames, fieldValues) <- frameRef + .thisObject() .filter(_ => compiler.scalaVersion.isScala2) .map(extractFields) .getOrElse(Safe((Nil, Nil))) @@ -162,71 +147,18 @@ private[internal] class ScalaEvaluator( } } - private def updateVariables(variableArray: JdiArray, frame: FrameReference): Safe[Unit] = { - def localVariables(): List[LocalVariable] = - // we must get a new StackFrame object after each invocation - frame.current().visibleVariables().asScala.toList - - val unboxedValues = localVariables() + private def updateVariables(variableArray: JdiArray, frame: JdiFrame): Safe[Unit] = { + val unboxedValues = frame + .variables() .zip(variableArray.getValues) .map { case (variable, value) => - if (isPrimitive(variable)) unboxIfPrimitive(value, frame.thread) + if (variable.`type`.isInstanceOf[PrimitiveType]) value.unboxIfPrimitive else Safe(value) } .traverse for (values <- unboxedValues) - yield { - for ((variable, value) <- localVariables().zip(values)) { - frame.current().setValue(variable, value) - } - } - } - - private def boxIfPrimitive(value: Value, classLoader: JdiClassLoader): Safe[Value] = { - val thread = classLoader.thread - value match { - case value: BooleanValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value: CharValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value: DoubleValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value: FloatValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value: IntegerValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value: LongValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value: ShortValue => - JdiPrimitive.box(value.value(), classLoader, thread) - case value => Safe(value) - } + yield for ((variable, value) <- frame.variables().zip(values)) + frame.setVariable(variable, value) } - - private val unboxMethods = Map( - "java.lang.Boolean" -> "booleanValue", - "java.lang.Byte" -> "byteValue", - "java.lang.Character" -> "charValue", - "java.lang.Double" -> "doubleValue", - "java.lang.Float" -> "floatValue", - "java.lang.Integer" -> "intValue", - "java.lang.Long" -> "longValue", - "java.lang.Short" -> "shortValue" - ) - - private def unboxIfPrimitive(value: Value, thread: ThreadReference): Safe[Value] = { - value match { - case ref: ObjectReference => - val typeName = ref.referenceType.name - unboxMethods - .get(typeName) - .map(methodName => new JdiObject(ref, thread).invoke(methodName, Nil)) - .getOrElse(Safe(value)) - case _ => Safe(value) - } - } - - private def isPrimitive(variable: LocalVariable): Boolean = - variable.`type`().isInstanceOf[PrimitiveType] } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/SimpleEvaluator.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/SimpleEvaluator.scala index f5643b02e..fe4fd1221 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/SimpleEvaluator.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/SimpleEvaluator.scala @@ -1,27 +1,28 @@ package ch.epfl.scala.debugadapter.internal.evaluator -import scala.jdk.CollectionConverters.* -import scala.util.Try +import ch.epfl.scala.debugadapter.Logger import com.sun.jdi.* -import scala.util.Success + +import scala.jdk.CollectionConverters.* import scala.util.Failure -import ch.epfl.scala.debugadapter.Logger +import scala.util.Success +import scala.util.Try class SimpleEvaluator(logger: Logger, testMode: Boolean) { - def prepare(expression: String, frame: FrameReference): Option[LocalValue] = { + def prepare(expression: String, frame: JdiFrame): Option[LocalValue] = { val encodedExpression = NameTransformer.encode(expression) if (isLocalVariable(frame, encodedExpression)) Some(LocalValue(encodedExpression)) else None } - def evaluate(localValue: LocalValue, frame: FrameReference): Try[Value] = Try { + def evaluate(localValue: LocalValue, frame: JdiFrame): Try[Value] = Try { val currentFrame = frame.current() val variable = currentFrame.visibleVariableByName(localValue.name) val rawValue = currentFrame.getValue(variable) - derefIfRef(rawValue, frame.thread) + JdiValue(rawValue, frame.thread).derefIfRef.value } - private def isLocalVariable(frame: FrameReference, name: String): Boolean = { + private def isLocalVariable(frame: JdiFrame, name: String): Boolean = { Try(frame.current().visibleVariables.asScala.toList) match { case Success(localVariables) => // we exclude the arguments of type scala.Function0 @@ -41,22 +42,4 @@ class SimpleEvaluator(logger: Logger, testMode: Boolean) { if (testMode) throw new Exception(message, throwable) else logger.warn(message) } - - private val refTypes = Set( - "scala.runtime.BooleanRef", - "scala.runtime.ByteRef", - "scala.runtime.CharRef", - "scala.runtime.DoubleRef", - "scala.runtime.FloatRef", - "scala.runtime.IntRef", - "scala.runtime.LongRef", - "scala.runtime.ShortRef", - "scala.runtime.ObjectRef" - ) - private def derefIfRef(value: Value, thread: ThreadReference): Value = - value match { - case ref: ObjectReference if refTypes.contains(ref.referenceType.name) => - new JdiObject(ref, thread).getFieldValue("elem") - case _ => value - } } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/package.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/package.scala index f0aa7a96e..5a51943e6 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/package.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/package.scala @@ -1,43 +1,6 @@ package ch.epfl.scala.debugadapter.internal -import com.sun.jdi._ - -import scala.jdk.CollectionConverters.* - package object evaluator { - private[evaluator] def method( - name: String, - referenceType: ReferenceType - ): Method = - referenceType.methodsByName(name).asScala.head - - private[evaluator] def method( - name: String, - signature: String, - referenceType: ReferenceType - ): Method = - referenceType.methodsByName(name, signature).asScala.head - - private[evaluator] def invokeMethod( - objRef: ObjectReference, - method: Method, - args: List[Value], - thread: ThreadReference - ): Safe[Value] = { - Safe(objRef.invokeMethod(thread, method, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED)) - .recoverWith { case t: InvocationException => - extractMessage(t, thread) - .map(message => throw new MethodInvocationFailed(message, t.exception())) - } - } - - private def extractMessage(invocationException: InvocationException, thread: ThreadReference): Safe[String] = { - val exception = invocationException.exception() - val getMessageMethod = method("toString", exception.referenceType()) - val message = invokeMethod(exception, getMessageMethod, List(), thread) - message.map(_.toString).recover { case _ => "" } - } - implicit class SafeSeq[A](seq: Seq[Safe[A]]) { def traverse: Safe[Seq[A]] = { seq.foldRight(Safe(Seq.empty[A])) { (safeHead, safeTail) => From 8d0ca5c1e50e6a14f43410b45e497efeb535426e Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 3 May 2023 15:14:03 +0200 Subject: [PATCH 2/2] Apply suggested changes --- .../internal/EvaluationProvider.scala | 6 +++--- .../internal/evaluator/JdiArray.scala | 12 +++++------ .../internal/evaluator/JdiClass.scala | 9 ++++----- .../internal/evaluator/JdiClassLoader.scala | 4 ++-- .../internal/evaluator/JdiFrame.scala | 10 ++++++---- .../internal/evaluator/JdiObject.scala | 1 + .../internal/evaluator/JdiValue.scala | 11 +++++----- .../internal/evaluator/ScalaEvaluator.scala | 20 +++++++++++-------- 8 files changed, 40 insertions(+), 33 deletions(-) diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala index 9f04f7bcd..e699a317a 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/EvaluationProvider.scala @@ -97,15 +97,15 @@ private[internal] class EvaluationProvider( thisContext: ObjectReference, methodName: String, methodSignature: String, - rawArgs: Array[Value], + args: Array[Value], thread: ThreadReference, invokeSuper: Boolean ): CompletableFuture[Value] = { val obj = JdiObject(thisContext, thread) - val args = if (rawArgs == null) Seq.empty else rawArgs.toSeq.map(JdiValue(_, thread)) + val wrappedArgs = if (args == null) Seq.empty else args.toSeq.map(JdiValue(_, thread)) val invocation = evaluationBlock { obj - .invoke(methodName, methodSignature, args) + .invoke(methodName, methodSignature, wrappedArgs) .recover { // if invocation throws an exception, we return that exception as the result case MethodInvocationFailed(msg, exception) => exception diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala index 41eda6ed8..b75d59eff 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiArray.scala @@ -6,16 +6,16 @@ import com.sun.jdi.Value import scala.jdk.CollectionConverters.* -class JdiArray(reference: ArrayReference, thread: ThreadReference) extends JdiObject(reference, thread) { +class JdiArray(arrayRef: ArrayReference, thread: ThreadReference) extends JdiObject(arrayRef, thread) { def setValue(index: Int, value: Value): Unit = - reference.setValue(index, value) + arrayRef.setValue(index, value) - def setValues(values: Seq[JdiValue]): Unit = reference.setValues(values.map(_.value).asJava) + def setValues(values: Seq[JdiValue]): Unit = arrayRef.setValues(values.map(_.value).asJava) - def getValues: Seq[JdiValue] = reference.getValues.asScala.toSeq.map(JdiValue(_, thread)) + def getValues: Seq[JdiValue] = arrayRef.getValues.asScala.toSeq.map(JdiValue(_, thread)) } object JdiArray { - def apply(value: Value, thread: ThreadReference): JdiArray = - new JdiArray(value.asInstanceOf[ArrayReference], thread) + def apply(arrayValue: Value, thread: ThreadReference): JdiArray = + new JdiArray(arrayValue.asInstanceOf[ArrayReference], thread) } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala index 73da5f660..93518cc41 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClass.scala @@ -52,7 +52,6 @@ private[internal] class JdiClass( Safe(cls.getValue(cls.fieldByName(fieldName))).map(JdiValue(_, thread)) def invokeStatic(methodName: String, args: Seq[JdiValue]): Safe[JdiValue] = { - cls.methodsByName(methodName).forEach(m => println(m.signature)) val method = cls.methodsByName(methodName).asScala.head invokeStatic(method, args) } @@ -62,16 +61,16 @@ private[internal] class JdiClass( invokeStatic(method, args) } - protected def invokeStatic(method: Method, args: Seq[JdiValue]): Safe[JdiValue] = + private def invokeStatic(method: Method, args: Seq[JdiValue]): Safe[JdiValue] = Safe(cls.invokeMethod(thread, method, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED)) .map(JdiValue(_, thread)) .recoverWith(recoverInvocationException(thread)) } object JdiClass { - def apply(classType: ReferenceType, thread: ThreadReference): JdiClass = + def apply(classType: Type, thread: ThreadReference): JdiClass = new JdiClass(classType.asInstanceOf[ClassType], thread) - def apply(classObject: ClassObjectReference, thread: ThreadReference): JdiClass = - new JdiClass(classObject.reflectedType.asInstanceOf[ClassType], thread) + def apply(classObject: Value, thread: ThreadReference): JdiClass = + JdiClass(classObject.asInstanceOf[ClassObjectReference].reflectedType, thread) } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala index e54ca537c..0f11ef069 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiClassLoader.scala @@ -90,6 +90,6 @@ private[internal] class JdiClassLoader( } private[internal] object JdiClassLoader { - def apply(ref: ClassLoaderReference, thread: ThreadReference): JdiClassLoader = - new JdiClassLoader(ref, thread) + def apply(ref: Value, thread: ThreadReference): JdiClassLoader = + new JdiClassLoader(ref.asInstanceOf[ClassLoaderReference], thread) } diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala index 740ea7f17..bf5c12473 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiFrame.scala @@ -11,7 +11,7 @@ final case class JdiFrame(thread: ThreadReference, depth: Int) { def current(): StackFrame = thread.frame(depth) // it's a Safe because it can fail, but it could also be a Try - def classLoader(): Safe[JdiClassLoader] = Safe { + def classLoader(): Safe[JdiClassLoader] = { def getClassLoaderRecursively(depth: Int): Option[ClassLoaderReference] = if (depth == thread.frameCount) None else { @@ -19,9 +19,11 @@ final case class JdiFrame(thread: ThreadReference, depth: Int) { .orElse(getClassLoaderRecursively(depth + 1)) } - val classLoader = getClassLoaderRecursively(depth) - .getOrElse(throw new Exception("Cannot find any classloader in the stack trace")) - JdiClassLoader(classLoader, thread) + Safe { + val classLoader = getClassLoaderRecursively(depth) + .getOrElse(throw new Exception("Cannot find any classloader in the stack trace")) + JdiClassLoader(classLoader, thread) + } } // this object can be null diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala index b11853a05..71743ba96 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiObject.scala @@ -45,6 +45,7 @@ private[internal] class JdiObject( .map(_.asString.stringValue) .recover { case _ => "" } + // we use a Seq instead of a Map because the ScalaEvaluator rely on the order of the fields def fields: Seq[(String, JdiValue)] = reference.referenceType.fields.asScala.toSeq .map(f => (f.name, JdiValue(reference.getValue(f), thread))) diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala index 88e8ec6bd..0f8a5ac08 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/JdiValue.scala @@ -3,10 +3,10 @@ package ch.epfl.scala.debugadapter.internal.evaluator import com.sun.jdi._ private[internal] class JdiValue(val value: Value, val thread: ThreadReference) { - def asObject: JdiObject = JdiObject(value.asInstanceOf[ObjectReference], thread) - def asClass: JdiClass = JdiClass(value.asInstanceOf[ClassObjectReference], thread) - def asClassLoader: JdiClassLoader = JdiClassLoader(value.asInstanceOf[ClassLoaderReference], thread) - def asArray: JdiArray = JdiArray(value.asInstanceOf[ArrayReference], thread) + def asObject: JdiObject = JdiObject(value, thread) + def asClass: JdiClass = JdiClass(value, thread) + def asClassLoader: JdiClassLoader = JdiClassLoader(value, thread) + def asArray: JdiArray = JdiArray(value, thread) def asString: JdiString = new JdiString(value.asInstanceOf[StringReference], thread) def unboxIfPrimitive: Safe[JdiValue] = { @@ -15,12 +15,13 @@ private[internal] class JdiValue(val value: Value, val thread: ThreadReference) val typeName = ref.referenceType.name JdiValue.unboxMethods .get(typeName) - .map(methodName => asObject.invoke(methodName, Nil)) + .map(methodName => JdiObject(ref, thread).invoke(methodName, Nil)) .getOrElse(Safe(this)) case _ => Safe(this) } } + // The ref types are used by the compiler to mutate local vars in nested methods def derefIfRef: JdiValue = value match { case ref: ObjectReference if JdiValue.refTypes.contains(ref.referenceType.name) => diff --git a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala index 407cc00dd..42a609d0a 100644 --- a/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala +++ b/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala @@ -106,6 +106,8 @@ private[internal] class ScalaEvaluator( ): Safe[(Seq[JdiString], Seq[JdiValue])] = { def extractVariablesFromFrame(): Safe[(Seq[JdiString], Seq[JdiValue])] = { val localVariables = frameRef.variablesAndValues().map { case (variable, value) => (variable.name, value) } + // Exclude the this object if there already is a local $this variable + // The Scala compiler uses `$this` in the extension methods of AnyVal classes val thisObject = frameRef.thisObject().filter(_ => !localVariables.contains("$this")).map("$this".->) (localVariables ++ thisObject) .map { case (name, value) => @@ -148,17 +150,19 @@ private[internal] class ScalaEvaluator( } private def updateVariables(variableArray: JdiArray, frame: JdiFrame): Safe[Unit] = { - val unboxedValues = frame + frame .variables() .zip(variableArray.getValues) - .map { case (variable, value) => - if (variable.`type`.isInstanceOf[PrimitiveType]) value.unboxIfPrimitive - else Safe(value) + .map { + case (variable, value) if variable.`type`.isInstanceOf[PrimitiveType] => value.unboxIfPrimitive + case (_, value) => Safe(value) } .traverse - - for (values <- unboxedValues) - yield for ((variable, value) <- frame.variables().zip(values)) - frame.setVariable(variable, value) + .map { values => + frame + .variables() + .zip(values) + .foreach { case (variable, value) => frame.setVariable(variable, value) } + } } }