Skip to content

Commit

Permalink
Report runtime validation error if expression compiler cannot be reso…
Browse files Browse the repository at this point in the history
…lved
  • Loading branch information
adpi2 committed Feb 6, 2024
1 parent 0754afa commit 801e435
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ch.epfl.scala.debugadapter.EvaluationFailed
import ch.epfl.scala.debugadapter.JavaRuntime
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.*
import com.microsoft.java.debug.core.IEvaluatableBreakpoint
import com.microsoft.java.debug.core.adapter.IDebugAdapterContext
Expand All @@ -27,9 +26,9 @@ import ScalaExtension.*

private[internal] class EvaluationProvider(
sourceLookUp: SourceLookUpProvider,
messageLogger: MessageLogger,
scalaEvaluators: Map[ClassEntry, ScalaEvaluator],
mode: DebugConfig.EvaluationMode,
compilers: Map[ClassEntry, ExpressionCompiler],
evaluationMode: DebugConfig.EvaluationMode,
testMode: Boolean,
logger: Logger
) extends IEvaluationProvider {

Expand Down Expand Up @@ -65,15 +64,13 @@ private[internal] class EvaluationProvider(
val location = frame.current().location
val locationCode = (location.method.name, location.codeIndex).hashCode
val expression =
if (breakpoint.getCompiledExpression(locationCode) != null) {
if (breakpoint.getCompiledExpression(locationCode) != null)
breakpoint.getCompiledExpression(locationCode).asInstanceOf[Try[PreparedExpression]]
} else if (breakpoint.containsConditionalExpression) {
else if (breakpoint.containsConditionalExpression)
prepare(breakpoint.getCondition, frame, preEvaluation = false)
} else if (breakpoint.containsLogpointExpression) {
else if (breakpoint.containsLogpointExpression)
prepareLogMessage(breakpoint.getLogMessage, frame)
} else {
Failure(new Exception("Missing expression"))
}
else Failure(new Exception("Missing expression"))
breakpoint.setCompiledExpression(locationCode, expression)
val evaluation = for {
expression <- expression
Expand Down Expand Up @@ -104,59 +101,60 @@ private[internal] class EvaluationProvider(
completeFuture(invocation.getResult, thread)
}

private def getScalaEvaluator(fqcn: String): Try[ScalaEvaluator] =
for {
entry <- sourceLookUp.getClassEntry(fqcn).toTry(s"Unknown class $fqcn")
evaluator <- scalaEvaluators.get(entry).toTry(missingEvaluatorMessage(entry))
} yield evaluator

private def missingEvaluatorMessage(entry: ClassEntry): String =
entry match {
case m: ManagedEntry =>
m.scalaVersion match {
case None => s"Unsupported evaluation in Java classpath entry: ${entry.name}"
case Some(sv) =>
s"""|Missing scala-expression-compiler_$sv with version ${BuildInfo.version}.
|You can open an issue at https://github.com/scalacenter/scala-debug-adapter.""".stripMargin
}
case _: JavaRuntime => s"Unsupported evaluation in JDK: ${entry.name}"
case _: UnmanagedEntry => s"Unsupported evaluation in unmanaged classpath entry: ${entry.name}"
case _ => s"Unsupported evaluation in ${entry.name}"
}

private def prepareLogMessage(message: String, frame: JdiFrame): Try[PreparedExpression] = {
if (!message.contains("$")) {
Success(PlainLogMessage(message))
} else if (mode.allowScalaEvaluation) {
} else if (evaluationMode.allowScalaEvaluation) {
val tripleQuote = "\"\"\""
val expression = s"""println(s$tripleQuote$message$tripleQuote)"""
compile(expression, frame)
} else Failure(new EvaluationFailed(s"Cannot evaluate logpoint '$message' with $mode mode"))
getScalaEvaluator(frame).flatMap(_.compile(expression))
} else Failure(new EvaluationFailed(s"Cannot evaluate logpoint '$message' with $evaluationMode mode"))
}

private def prepare(expression: String, frame: JdiFrame, preEvaluation: Boolean): Try[PreparedExpression] =
if (mode.allowRuntimeEvaluation)
private def prepare(expression: String, frame: JdiFrame, preEvaluation: Boolean): Try[PreparedExpression] = {
val scalaEvaluator = getScalaEvaluator(frame)
def compiledExpression = scalaEvaluator.flatMap(_.compile(expression))
if (evaluationMode.allowRuntimeEvaluation) {
runtimeEvaluator.validate(expression, frame, preEvaluation) match {
case Success(expr) if mode.allowScalaEvaluation && containsMethodCall(expr.tree) =>
compile(expression, frame).orElse(Success(expr))
case Success(expr) if scalaEvaluator.isSuccess && containsMethodCall(expr.tree) =>
Success(compiledExpression.getOrElse(expr))
case success: Success[RuntimeExpression] => success
case failure: Failure[RuntimeExpression] =>
if (mode.allowScalaEvaluation) compile(expression, frame) else failure
if (scalaEvaluator.isSuccess) compiledExpression
else failure
}
else if (mode.allowScalaEvaluation) compile(expression, frame)
} else if (evaluationMode.allowScalaEvaluation) compiledExpression
else Failure(new EvaluationFailed(s"Evaluation is disabled"))

private def compile(expression: String, frame: JdiFrame): Try[CompiledExpression] = {
val fqcn = frame.current().location.declaringType.name
for {
evaluator <- getScalaEvaluator(fqcn)
sourceContent <- sourceLookUp
.getSourceContentFromClassName(fqcn)
.toTry(s"Cannot find source file of class $fqcn")
preparedExpression <- evaluator.compile(sourceContent, expression, frame)
} yield preparedExpression
}

private def getScalaEvaluator(frame: JdiFrame): Try[ScalaEvaluator] =
if (evaluationMode.allowScalaEvaluation)
for {
fqcn <- Try(frame.current().location.declaringType.name)
entry <- sourceLookUp.getClassEntry(fqcn).toTry(s"Unknown class $fqcn")
compiler <- compilers.get(entry).toTry(missingCompilerMessage(entry))
sourceContent <- sourceLookUp
.getSourceContentFromClassName(fqcn)
.toTry(s"Cannot find source file of class $fqcn")
} yield new ScalaEvaluator(sourceContent, frame, compiler, logger, testMode)
else Failure(new Exception("Scala evaluation is not allowed"))

private def missingCompilerMessage(entry: ClassEntry): String =
entry match {
case m: ManagedEntry =>
m.scalaVersion match {
case None =>
s"Failed resolving scala-expression-compiler:${BuildInfo.version} for ${entry.name} (Missing Scala Version)"
case Some(sv) =>
s"""|Failed resolving scala-expression-compiler:${BuildInfo.version} for Scala $sv.
|Please open an issue at https://github.com/scalacenter/scala-debug-adapter.""".stripMargin
}
case _: JavaRuntime =>
s"Failed resolving scala-expression-compiler:${BuildInfo.version} for ${entry.name} (Missing Scala Version)"
case _ =>
s"Failed resolving scala-expression-compiler:${BuildInfo.version} for ${entry.name} (Unknown Scala Version)"
}

private def containsMethodCall(tree: RuntimeEvaluationTree): Boolean = {
import RuntimeEvaluationTree.*
tree match {
Expand All @@ -173,13 +171,12 @@ private[internal] class EvaluationProvider(

private def evaluate(expression: PreparedExpression, frame: JdiFrame): Try[Value] = evaluationBlock {
expression match {
case logMessage: PlainLogMessage => messageLogger.log(logMessage, frame)
case logMessage: PlainLogMessage => MessageLogger.log(logMessage, frame)
case expr: RuntimeExpression => runtimeEvaluator.evaluate(expr, frame)
case expr: CompiledExpression =>
val fqcn = frame.current().location.declaringType.name
for {
scalaEvaluator <- getScalaEvaluator(fqcn)
compiledExpression <- scalaEvaluator.evaluate(expr, frame)
scalaEvaluator <- getScalaEvaluator(frame)
compiledExpression <- scalaEvaluator.evaluate(expr)
} yield compiledExpression
}
}
Expand All @@ -204,22 +201,12 @@ private[internal] class EvaluationProvider(
}

private[internal] object EvaluationProvider {
def apply(
debuggee: Debuggee,
tools: DebugTools,
logger: Logger,
config: DebugConfig
): IEvaluationProvider = {
val scalaEvaluators = tools.expressionCompilers.view.map { case (entry, compiler) =>
(entry, new ScalaEvaluator(entry, compiler, logger, config.testMode))
}.toMap
val messageLogger = new MessageLogger()
def apply(debuggee: Debuggee, tools: DebugTools, logger: Logger, config: DebugConfig): IEvaluationProvider =
new EvaluationProvider(
tools.sourceLookUp,
messageLogger,
scalaEvaluators,
tools.expressionCompilers,
config.evaluationMode,
config.testMode,
logger
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.sun.jdi.*

import scala.util.Try

private[internal] class MessageLogger() {
private[internal] object MessageLogger {
def log(logMessage: PlainLogMessage, frame: JdiFrame): Try[Value] = {
val result = for {
classLoader <- frame.classLoader()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private[evaluator] class RuntimeValidation(frame: JdiFrame, sourceLookUp: Source
case instance: Term.New => validateNew(instance)
case block: Term.Block => validateBlock(block)
case assign: Term.Assign => validateAssign(assign)
case _ => Recoverable("Expression not supported at runtime")
case _ => Recoverable(s"Cannot evaluate '$expression' at runtime")
}

private def validateAsValueOrClass(expression: Stat): Validation[Either[RuntimeEvaluationTree, jdi.ReferenceType]] =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package ch.epfl.scala.debugadapter.internal.evaluator

import ch.epfl.scala.debugadapter.ClassEntry
import ch.epfl.scala.debugadapter.Logger
import com.sun.jdi._

Expand All @@ -10,15 +9,16 @@ import java.nio.file.Path
import scala.util.Try

private[internal] class ScalaEvaluator(
entry: ClassEntry,
sourceContent: String,
frame: JdiFrame,
compiler: ExpressionCompiler,
logger: Logger,
testMode: Boolean
) {
def evaluate(expression: CompiledExpression, frame: JdiFrame): Try[Value] =
evaluate(expression.classDir, expression.className, frame)
def evaluate(expression: CompiledExpression): Try[Value] =
evaluate(expression.classDir, expression.className)

def compile(sourceContent: String, expression: String, frame: JdiFrame): Try[CompiledExpression] = {
def compile(expression: String): Try[CompiledExpression] = {
logger.debug(s"Compiling expression '$expression'")
val location = frame.current().location
val line = location.lineNumber
Expand All @@ -38,7 +38,7 @@ private[internal] class ScalaEvaluator(
val compiledExpression =
for {
classLoader <- frame.classLoader()
(names, values) <- extractValuesAndNames(frame, classLoader)
(names, values) <- extractValuesAndNames(classLoader)
localNames = names.map(_.stringValue).toSet
_ <- compiler
.compile(outDir, expressionClassName, sourceFile, line, expression, localNames, packageName, testMode)
Expand All @@ -47,16 +47,16 @@ private[internal] class ScalaEvaluator(
compiledExpression.getResult
}

private def evaluate(classDir: Path, className: String, frame: JdiFrame): Try[Value] = {
private def evaluate(classDir: Path, className: String): Try[Value] = {
val evaluatedValue = for {
classLoader <- frame.classLoader()
(names, values) <- extractValuesAndNames(frame, classLoader)
(names, values) <- extractValuesAndNames(classLoader)
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)
_ <- updateVariables(valuesArray)
unboxedValue <- evaluatedValue.unboxIfPrimitive
} yield unboxedValue.value
evaluatedValue.getResult
Expand Down Expand Up @@ -97,15 +97,12 @@ private[internal] class ScalaEvaluator(
* - fields from this object
* @return Tuple of extracted names and values
*/
private def extractValuesAndNames(
frameRef: JdiFrame,
classLoader: JdiClassLoader
): Safe[(Seq[JdiString], Seq[JdiValue])] = {
private def extractValuesAndNames(classLoader: JdiClassLoader): Safe[(Seq[JdiString], Seq[JdiValue])] = {
def extractVariablesFromFrame(): Safe[(Seq[JdiString], Seq[JdiValue])] = {
val localVariables = frameRef.variablesAndValues().map { case (variable, value) => (variable.name, value) }
val localVariables = frame.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".->)
val thisObject = frame.thisObject.filter(_ => !localVariables.contains("$this")).map("$this".->)
(localVariables ++ thisObject)
.map { case (name, value) =>
for {
Expand All @@ -131,7 +128,7 @@ private[internal] class ScalaEvaluator(
// expression evaluator.
// It is dangerous because local values can shadow fields
// TODO: adapt Scala 2 expression compiler
(fieldNames, fieldValues) <- frameRef.thisObject
(fieldNames, fieldValues) <- frame.thisObject
.filter(_ => compiler.scalaVersion.isScala2)
.map(extractFields)
.getOrElse(Safe((Nil, Nil)))
Expand All @@ -145,7 +142,7 @@ private[internal] class ScalaEvaluator(
}
}

private def updateVariables(variableArray: JdiArray, frame: JdiFrame): Safe[Unit] = {
private def updateVariables(variableArray: JdiArray): Safe[Unit] = {
frame
.variables()
.zip(variableArray.getValues)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ def checkDebugJavaWithGSpecializedTask = Def.inputTask {
val uri = (Compile / startMainClassDebugSession).evaluated
val source = (Compile / sources).value.head.toPath
implicit val context: TestingContext = TestingContext(source, "3.3.0")
DebugTest.check(uri)(Breakpoint(source, 6), Evaluation.failed("x", "Missing scala-expression-compiler"))
DebugTest.check(uri)(
Breakpoint(source, 6),
// report runtime validation error if expression compiler cannot be resolved.
Evaluation.failed("x", "x is not a local variable")
)
}

lazy val debugJavaWithG =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ checkDebugSession := {
val source = (Compile / sources).value.head.toPath

implicit val ctx = TestingContext(source, scalaVersion.value)
DebugTest.check(uri)(Breakpoint(4), Evaluation.failed("s\"${1 + 1}\"", "Missing scala-expression-compiler"))
DebugTest.check(uri)(Breakpoint(4), Evaluation.failed("s\"${1 + 1}\"", "Cannot evaluate"))
}

0 comments on commit 801e435

Please sign in to comment.