[JS IR] Emit original names for local vars to sourcemaps
#KT-35655 Fixed
This commit is contained in:
committed by
Space Team
parent
8efa72ca36
commit
7b7c517dbb
@@ -158,6 +158,10 @@ fun main(args: Array<String>) {
|
||||
model("debug/stepping")
|
||||
}
|
||||
|
||||
testClass<AbstractIrJsLocalVariableTest> {
|
||||
model("debug/localVariables")
|
||||
}
|
||||
|
||||
testClass<AbstractFir2IrJsTextTest>(
|
||||
suiteTestClassName = "Fir2IrJsTextTestGenerated"
|
||||
) {
|
||||
|
||||
+43
@@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user