[JS IR] Emit original names for local vars to sourcemaps

#KT-35655 Fixed
This commit is contained in:
Sergej Jaskiewicz
2022-11-01 19:19:25 +01:00
committed by Space Team
parent 8efa72ca36
commit 7b7c517dbb
114 changed files with 1970 additions and 376 deletions
@@ -158,6 +158,10 @@ fun main(args: Array<String>) {
model("debug/stepping")
}
testClass<AbstractIrJsLocalVariableTest> {
model("debug/localVariables")
}
testClass<AbstractFir2IrJsTextTest>(
suiteTestClassName = "Fir2IrJsTextTestGenerated"
) {
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.js.test
import org.jetbrains.kotlin.test.directives.ConfigurationDirectives
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.AdditionalSourceProvider
import org.jetbrains.kotlin.test.services.ModuleStructureExtractor
import org.jetbrains.kotlin.test.services.TestServices
import java.io.File
class JsSteppingTestAdditionalSourceProvider(testServices: TestServices) : AdditionalSourceProvider(testServices) {
override fun produceAdditionalFiles(globalDirectives: RegisteredDirectives, module: TestModule): List<TestFile> {
// HACK: For some reason if we add the same additional files to each module (not only the main one),
// we get the 'Symbol already bound' exception during linking.
// The craziest part is that we run most of the box tests with additional files, where we _do_ add the same additional files
// to each module (using JsAdditionalSourceProvider), but the linker doesn't complain.
// For some reason, it doesn't like _specifically_ the symbols defined in compiler/testData/debug/jsTestHelpers.
return if (module.name == ModuleStructureExtractor.DEFAULT_MODULE_NAME) {
buildList {
if (containsDirective(globalDirectives, module, ConfigurationDirectives.WITH_STDLIB))
add(File(WITH_STDLIB_HELPER_PATH).toTestFile())
else
add(File(MINIMAL_HELPER_PATH).toTestFile())
add(File(COMMON_HELPER_PATH).toTestFile())
}
} else
emptyList()
}
companion object {
private const val HELPERS_DIR = "compiler/testData/debug/jsTestHelpers"
private const val COMMON_HELPER_PATH = "$HELPERS_DIR/jsCommonTestHelpers.kt"
private const val MINIMAL_HELPER_PATH = "$HELPERS_DIR/jsMinimalTestHelpers.kt"
private const val WITH_STDLIB_HELPER_PATH = "$HELPERS_DIR/jsWithStdlibTestHelpers.kt"
}
}
@@ -310,6 +310,9 @@ class Runtime(private val requestEvaluator: CDPRequestEvaluator) {
@SerialName("symbol")
SYMBOL,
@SerialName("accessor")
ACCESSOR,
@SerialName("bigint")
BIGINT,
}
@@ -396,6 +399,10 @@ class Runtime(private val requestEvaluator: CDPRequestEvaluator) {
* Object class (constructor) name. Specified for [ValueType.OBJECT] type values only.
*/
val className: String? = null,
/**
* Remote object value in case of primitive values or JSON values (if it was requested).
*/
val value: JsonElement? = null,
/**
* String representation of the object.
*/
@@ -403,7 +410,7 @@ class Runtime(private val requestEvaluator: CDPRequestEvaluator) {
/**
* Unique object identifier (for non-primitive values).
*/
val objectId: RemoteObjectId? = null
val objectId: RemoteObjectId? = null,
)
/**
@@ -718,7 +725,11 @@ class Debugger(private val requestEvaluator: CDPRequestEvaluator) {
}
@Serializable
private class EvaluateOnCallFrameRequestParams(val callFrameId: CallFrameId, val expression: String) : CDPRequestParams()
private class EvaluateOnCallFrameRequestParams(
val callFrameId: CallFrameId,
val expression: String,
val returnByValue: Boolean? = null,
) : CDPRequestParams()
/**
* Evaluates expression on a given call frame.
@@ -727,15 +738,19 @@ class Debugger(private val requestEvaluator: CDPRequestEvaluator) {
*
* @param callFrameId Call frame identifier to evaluate on.
* @param expression Expression to evaluate.
* @param returnByValue Whether the result is expected to be a JSON object that should be sent by value.
*/
suspend fun evaluateOnCallFrame(callFrameId: CallFrameId, expression: String) =
requestEvaluator.evaluateRequest<Runtime.EvaluationResult> { messageId ->
encodeCDPMethodCall<Runtime.EvaluationResult, EvaluateOnCallFrameRequestParams>(
messageId,
"Debugger.evaluateOnCallFrame",
EvaluateOnCallFrameRequestParams(callFrameId, expression)
)
}
suspend fun evaluateOnCallFrame(
callFrameId: CallFrameId,
expression: String,
returnByValue: Boolean? = null,
) = requestEvaluator.evaluateRequest<Runtime.EvaluationResult> { messageId ->
encodeCDPMethodCall<Runtime.EvaluationResult, EvaluateOnCallFrameRequestParams>(
messageId,
"Debugger.evaluateOnCallFrame",
EvaluateOnCallFrameRequestParams(callFrameId, expression, returnByValue)
)
}
/**
* Breakpoint identifier.
@@ -803,6 +818,11 @@ class Debugger(private val requestEvaluator: CDPRequestEvaluator) {
*/
val location: Location,
/**
* Scope chain for this call frame.
*/
val scopeChain: List<Scope>,
/**
* `this` object for this call frame.
*/
@@ -814,6 +834,66 @@ class Debugger(private val requestEvaluator: CDPRequestEvaluator) {
val returnValue: Runtime.RemoteObject? = null,
)
@Serializable
class Scope private constructor(
/**
* Scope type.
*/
val type: ScopeType,
/**
* Object representing the scope. For [ScopeType.GLOBAL] and [Scopetype.WITH] scopes it represents the actual object;
* for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties.
*/
val `object`: Runtime.RemoteObject,
val name: String? = null,
/**
* Location in the source code where scope starts
*/
val startLocation: Location? = null,
/**
* Location in the source code where scope ends
*/
val endLocation: Location? = null,
)
@Serializable
enum class ScopeType {
@SerialName("global")
GLOBAL,
@SerialName("local")
LOCAL,
@SerialName("with")
WITH,
@SerialName("closure")
CLOSURE,
@SerialName("catch")
CATCH,
@SerialName("block")
BLOCK,
@SerialName("script")
SCRIPT,
@SerialName("eval")
EVAL,
@SerialName("module")
MODULE,
@SerialName("wasm-expression-stack")
WASM_EXPRESSION_STACK,
}
@Serializable
enum class PauseReason {
@@ -16,8 +16,11 @@ try {
vm.runInContext(code, sandbox, testFilePath);
// language=JavaScript
vm.runInContext(`
const __continuation = main.testUtils.makeEmptyContinuation();
// noinspection JSUnusedLocalSymbols (called in debugger, see JsDebugRunner)
const __makeValueDescriptionForSteppingTests = main.testUtils.makeValueDescriptionForSteppingTests;
debugger;
main.box();
main.box(__continuation);
`,
sandbox
);
@@ -4,8 +4,16 @@
*/
package org.jetbrains.kotlin.js.test.handlers
import com.google.gwt.dev.js.ThrowExceptionOnErrorReporter
import com.google.gwt.dev.js.rhino.CodePosition
import com.google.gwt.dev.js.rhino.offsetOf
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.parser.parseFunction
import org.jetbrains.kotlin.js.parser.sourcemaps.*
import org.jetbrains.kotlin.js.test.debugger.*
import org.jetbrains.kotlin.js.test.utils.getAllFilesForRunner
@@ -15,9 +23,7 @@ import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.configuration.JsEnvironmentConfigurator
import org.jetbrains.kotlin.test.services.moduleStructure
import org.jetbrains.kotlin.test.utils.SteppingTestLoggedData
import org.jetbrains.kotlin.test.utils.checkSteppingTestResult
import org.jetbrains.kotlin.test.utils.formatAsSteppingTestExpectation
import org.jetbrains.kotlin.test.utils.*
import java.io.File
import java.net.URI
import java.net.URISyntaxException
@@ -39,7 +45,7 @@ import java.net.URISyntaxException
* supported.
*
*/
class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(testServices) {
class JsDebugRunner(testServices: TestServices, private val localVariables: Boolean) : AbstractJsArtifactsCollector(testServices) {
override fun processAfterAllModules(someAssertionWasFailed: Boolean) {
if (someAssertionWasFailed) return
@@ -69,7 +75,7 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
mainModule: TestModule,
) {
val originalFile = mainModule.files.first { !it.isAdditional }.originalFile
val debuggerFacade = NodeJsDebuggerFacade(jsFilePath)
val debuggerFacade = NodeJsDebuggerFacade(jsFilePath, localVariables)
val jsFile = File(jsFilePath)
@@ -105,7 +111,7 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
repeatedlyStepInto { callFrame ->
callFrame.isInFileUnderTest().also {
if (it)
addCallFrameInfoToLoggedItems(sourceMap, callFrame, loggedItems)
addCallFrameInfoToLoggedItems(jsFile, sourceMap, callFrame, loggedItems)
}
}
@@ -120,7 +126,8 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
)
}
private fun addCallFrameInfoToLoggedItems(
private suspend fun NodeJsDebuggerFacade.Context.addCallFrameInfoToLoggedItems(
jsFile: File,
sourceMap: SourceMap,
topMostCallFrame: Debugger.CallFrame,
loggedItems: MutableList<SteppingTestLoggedData>
@@ -139,6 +146,7 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
sourceLine + 1,
originalFunctionName ?: topMostCallFrame.functionName,
false,
getLocalVariables(jsFile, sourceMap, topMostCallFrame),
)
loggedItems.add(SteppingTestLoggedData(sourceLine + 1, false, expectation))
}
@@ -164,7 +172,7 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
*
* @param jsFilePath the test file to execute and debug.
*/
private class NodeJsDebuggerFacade(jsFilePath: String) {
private class NodeJsDebuggerFacade(jsFilePath: String, private val localVariables: Boolean) {
private val inspector =
NodeJsInspectorClient("js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js", listOf(jsFilePath))
@@ -173,6 +181,8 @@ private class NodeJsDebuggerFacade(jsFilePath: String) {
private var pausedEvent: Debugger.Event.Paused? = null
private val sourceCache = mutableMapOf<URI, String>()
init {
inspector.onEvent { event ->
when (event) {
@@ -220,6 +230,100 @@ private class NodeJsDebuggerFacade(jsFilePath: String) {
}
suspend fun waitForResumeEvent() = waitForConditionToBecomeTrue { pausedEvent == null }
suspend fun getLocalVariables(
jsFile: File,
sourceMap: SourceMap,
callFrame: Debugger.CallFrame
): List<LocalVariableRecord>? {
if (!localVariables) return null
val functionScope = callFrame.scopeChain.find { it.type in setOf(Debugger.ScopeType.LOCAL, Debugger.ScopeType.CLOSURE) }
?: return null
val scopeStart = functionScope.startLocation?.toCodePosition() ?: error("Missing scope location")
val scopeEnd = functionScope.endLocation?.toCodePosition() ?: error("Missing scope location")
val jsFileURI = jsFile.makeURI()
require(URI(scriptUrlByScriptId(functionScope.startLocation.scriptId)) == jsFileURI) {
"Invalid scope location: $scopeStart. Expected scope location to be in $jsFile"
}
val sourceText = sourceCache.getOrPut(jsFileURI, jsFile::readText)
val scopeText = sourceText.let {
it.substring(it.offsetOf(scopeStart), it.offsetOf(scopeEnd))
}
val prefix = "function"
// Function scope starts with an open paren, so we need to add the keyword to make it valid JavaScript.
// TODO: This will not work with arrows. As of 2022 we don't generate them, but we might in the future.
val parseableScopeText = prefix + scopeText
val scope = JsProgram().scope
val jsFunction = parseFunction(
parseableScopeText,
jsFile.name,
CodePosition(scopeStart.line, scopeStart.offset - prefix.length),
0,
ThrowExceptionOnErrorReporter,
scope
) ?: error("Could not parse scope: \n$parseableScopeText")
val variables = mutableListOf<SourceInfoAwareJsNode /* JsVars.JsVar | JsParameter */>()
object : JsVisitor() {
override fun visitElement(node: JsNode) {
node.acceptChildren(this)
}
override fun visit(x: JsVars.JsVar) {
super.visit(x)
variables.add(x)
}
override fun visitParameter(x: JsParameter) {
super.visitParameter(x)
variables.add(x)
}
}.accept(jsFunction)
val nameMapping = variables.mapNotNull { variable ->
if (variable !is HasName) error("Unexpected JsNode: $variable")
// Filter out variables declared in nested functions
if (!jsFunction.scope.hasOwnName(variable.name.toString())) return@mapNotNull null
val location = variable.source
if (location !is JsLocation?) error("JsLocation expected. Found instead: $location")
if (location == null)
null
else sourceMap.segmentForGeneratedLocation(location.startLine, location.startChar)?.name?.let {
it to variable.name.toString()
}
}
if (nameMapping.isEmpty()) return emptyList()
val expression = nameMapping.joinToString(separator = ",", prefix = "[", postfix = "]") { (_, generatedName) ->
"__makeValueDescriptionForSteppingTests($generatedName)"
}
val evaluationResult = debugger.evaluateOnCallFrame(callFrame.callFrameId, expression, returnByValue = true)
if (evaluationResult.exceptionDetails != null) {
evaluationResult.exceptionDetails.rethrow()
}
val valueDescriptions =
Json.Default.decodeFromJsonElement<List<ValueDescription?>>(evaluationResult.result.value ?: error("missing value"))
return nameMapping.mapIndexedNotNull { i, (originalName, _) ->
valueDescriptions[i]?.toLocalVariableRecord(originalName)
}
}
private fun Runtime.ExceptionDetails.rethrow(): Nothing {
if (exception?.description != null) error(exception.description)
if (scriptId == null) error(text)
val scriptURL = scriptUrls[scriptId] ?: url ?: error(text)
error("$text ($scriptURL:$lineNumber:$columnNumber)")
}
}
}
@@ -228,6 +332,8 @@ private fun File.makeURI(): URI = absoluteFile.toURI().withAuthority("")
private fun URI.withAuthority(newAuthority: String?) =
URI(scheme, newAuthority, path, query, fragment)
private fun Debugger.Location.toCodePosition() = CodePosition(lineNumber, columnNumber ?: -1)
private fun SourceMap.segmentForGeneratedLocation(lineNumber: Int, columnNumber: Int?): SourceMapSegment? {
val group = groups.getOrNull(lineNumber)?.takeIf { it.segments.isNotEmpty() } ?: return null
@@ -245,3 +351,16 @@ private fun SourceMap.segmentForGeneratedLocation(lineNumber: Int, columnNumber:
group.segments[candidateIndex - 1]
}
}
@Serializable
private class ValueDescription(val isNull: Boolean, val isReferenceType: Boolean, val valueDescription: String, val typeName: String) {
fun toLocalVariableRecord(variableName: String) = LocalVariableRecord(
variable = variableName,
variableType = null, // In JavaScript variables are untyped
value = when {
isNull -> LocalNullValue
isReferenceType -> LocalReference("", typeName)
else -> LocalPrimitive(valueDescription, typeName)
}
)
}
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.js.test.ir
import org.jetbrains.kotlin.js.test.AbstractJsBlackBoxCodegenTestBase
import org.jetbrains.kotlin.js.test.JsAdditionalSourceProvider
import org.jetbrains.kotlin.js.test.JsSteppingTestAdditionalSourceProvider
import org.jetbrains.kotlin.js.test.converters.JsIrBackendFacade
import org.jetbrains.kotlin.js.test.converters.JsKlibBackendFacade
import org.jetbrains.kotlin.js.test.converters.incremental.RecompileModuleJsIrBackendFacade
@@ -157,8 +158,25 @@ open class AbstractIrJsSteppingTest : AbstractJsIrTest(
defaultDirectives {
+JsEnvironmentConfigurationDirectives.NO_COMMON_FILES
}
useAdditionalSourceProviders(::JsSteppingTestAdditionalSourceProvider)
jsArtifactsHandlersStep {
useHandlers(::JsDebugRunner)
useHandlers({ JsDebugRunner(it, localVariables = false) })
}
}
}
open class AbstractIrJsLocalVariableTest : AbstractJsIrTest(
pathToTestDir = "compiler/testData/debug/localVariables/",
testGroupOutputDirPrefix = "debug/localVariables/"
) {
override fun TestConfigurationBuilder.configuration() {
commonConfigurationForJsBlackBoxCodegenTest()
defaultDirectives {
+JsEnvironmentConfigurationDirectives.NO_COMMON_FILES
}
useAdditionalSourceProviders(::JsSteppingTestAdditionalSourceProvider)
jsArtifactsHandlersStep {
useHandlers({ JsDebugRunner(it, localVariables = true) })
}
}
}