[JS IR] Fix stack trace capturing in secondary constructors (KT-37563)

Call captureStack in primary constructors and generated factories
This commit is contained in:
Svyatoslav Kuzmich
2020-06-10 11:59:17 +03:00
parent dfdc1e9c4f
commit 6792779281
11 changed files with 171 additions and 13 deletions
@@ -270,6 +270,12 @@ fun usefulDeclarations(roots: Iterable<IrDeclaration>, context: JsIrBackendConte
expression.symbol.owner.enqueue("function access")
}
override fun visitRawFunctionReference(expression: IrRawFunctionReference) {
super.visitRawFunctionReference(expression)
expression.symbol.owner.enqueue("raw function access")
}
override fun visitVariableAccess(expression: IrValueAccessExpression) {
super.visitVariableAccess(expression)
@@ -301,6 +301,8 @@ class JsIntrinsics(private val irBuiltIns: IrBuiltIns, val context: JsIrBackendC
val jsBoxIntrinsic = getInternalFunction("boxIntrinsic")
val jsUnboxIntrinsic = getInternalFunction("unboxIntrinsic")
val captureStack = getInternalFunction("captureStack")
val createSharedBox = defineCreateSharedBox()
val readSharedBox = defineReadSharedBox()
val writeSharedBox = defineWriteSharedBox()
@@ -624,6 +624,12 @@ private val objectUsageLoweringPhase = makeBodyLoweringPhase(
description = "Transform IrGetObjectValue into instance generator call"
)
private val captureStackTraceInThrowablesPhase = makeBodyLoweringPhase(
::CaptureStackTraceInThrowables,
name = "CaptureStackTraceInThrowables",
description = "Capture stack trace in Throwable constructors"
)
private val cleanupLoweringPhase = makeBodyLoweringPhase(
{ CleanupLowering() },
name = "CleanupLowering",
@@ -704,6 +710,7 @@ val loweringList = listOf<Lowering>(
constLoweringPhase,
objectDeclarationLoweringPhase,
objectUsageLoweringPhase,
captureStackTraceInThrowablesPhase,
callsLoweringPhase,
cleanupLoweringPhase,
validateIrAfterLowering
@@ -784,6 +791,7 @@ val es6LoweringList = listOf<Lowering>(
constLoweringPhase,
objectDeclarationLoweringPhase,
objectUsageLoweringPhase,
captureStackTraceInThrowablesPhase,
callsLoweringPhase,
cleanupLoweringPhase,
validateIrAfterLowering
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2019 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.ir.backend.js.lower
import org.jetbrains.kotlin.backend.common.BodyLoweringPass
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder
import org.jetbrains.kotlin.ir.declarations.IrConstructor
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.expressions.IrBody
import org.jetbrains.kotlin.ir.expressions.impl.IrBlockBodyImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrRawFunctionReferenceImpl
import org.jetbrains.kotlin.ir.util.isSubclassOf
import org.jetbrains.kotlin.ir.util.parentAsClass
/**
* Capture stack trace in primary constructors of Throwable
*/
class CaptureStackTraceInThrowables(val context: JsIrBackendContext) : BodyLoweringPass {
override fun lower(irBody: IrBody, container: IrDeclaration) {
if (container !is IrConstructor || !container.isPrimary)
return
val klass = container.parentAsClass
if (!klass.isSubclassOf(context.irBuiltIns.throwableClass.owner))
return
(irBody as IrBlockBodyImpl).statements += JsIrBuilder.buildCall(context.intrinsics.captureStack).also { call ->
call.putValueArgument(0, JsIrBuilder.buildGetValue(klass.thisReceiver!!.symbol))
call.putValueArgument(
1,
IrRawFunctionReferenceImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.anyType, container.symbol)
)
}
}
}
@@ -17,18 +17,15 @@ import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder
import org.jetbrains.kotlin.ir.backend.js.utils.getJsName
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrBlockBodyImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrReturnImpl
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.isSubclassOf
import org.jetbrains.kotlin.ir.util.parentAsClass
import org.jetbrains.kotlin.ir.visitors.IrElementTransformer
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
@@ -99,9 +96,28 @@ class SecondaryConstructorLowering(val context: JsIrBackendContext) : Declaratio
call.putValueArgument(constructor.valueParameters.size, irCreateCall)
}
val irReturn = JsIrBuilder.buildReturn(stub.symbol, irDelegateCall, context.irBuiltIns.nothingType)
statements += irReturn
if (irClass.isSubclassOf(context.irBuiltIns.throwableClass.owner)) {
val tmp = JsIrBuilder.buildVar(
type = irDelegateCall.type,
parent = stub,
initializer = irDelegateCall
)
statements += tmp
statements += JsIrBuilder.buildCall(context.intrinsics.captureStack).also { call ->
call.putValueArgument(0, JsIrBuilder.buildGetValue(tmp.symbol))
call.putValueArgument(
1,
IrRawFunctionReferenceImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.anyType, stub.symbol)
)
}
statements += JsIrBuilder.buildReturn(stub.symbol, JsIrBuilder.buildGetValue(tmp.symbol), context.irBuiltIns.nothingType)
} else {
val irReturn = JsIrBuilder.buildReturn(stub.symbol, irDelegateCall, context.irBuiltIns.nothingType)
statements += irReturn
}
}
}
@@ -7,7 +7,10 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.backend.common.ir.isElseBranch
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.backend.js.utils.JsGenerationContext
import org.jetbrains.kotlin.ir.backend.js.utils.Namer
import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrConstructor
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
@@ -267,6 +270,15 @@ class IrElementToJsExpressionTransformer : BaseIrElementToJsNodeTransformer<JsEx
else -> error("Unexpected operator ${expression.operator}: ${expression.render()}")
}
override fun visitRawFunctionReference(expression: IrRawFunctionReference, data: JsGenerationContext): JsExpression {
val name = when (val function = expression.symbol.owner) {
is IrConstructor -> data.getNameForConstructor(function)
is IrSimpleFunction -> data.getNameForStaticFunction(function)
else -> error("Unexpected function kind")
}
return JsNameRef(name)
}
private fun prefixOperation(operator: JsUnaryOperator, expression: IrDynamicOperatorExpression, data: JsGenerationContext) =
JsPrefixOperation(
operator,
@@ -2621,6 +2621,11 @@ public class IrBoxJsES6TestGenerated extends AbstractIrBoxJsES6Test {
runTest("js/js.translator/testData/box/expression/misc/safeCallComputesExpressionOnlyOnce.kt");
}
@TestMetadata("stackTraceCapturing.kt")
public void testStackTraceCapturing() throws Exception {
runTest("js/js.translator/testData/box/expression/misc/stackTraceCapturing.kt");
}
@TestMetadata("stringInterpolationEvaluationOrder.kt")
public void testStringInterpolationEvaluationOrder() throws Exception {
runTest("js/js.translator/testData/box/expression/misc/stringInterpolationEvaluationOrder.kt");
@@ -2621,6 +2621,11 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest {
runTest("js/js.translator/testData/box/expression/misc/safeCallComputesExpressionOnlyOnce.kt");
}
@TestMetadata("stackTraceCapturing.kt")
public void testStackTraceCapturing() throws Exception {
runTest("js/js.translator/testData/box/expression/misc/stackTraceCapturing.kt");
}
@TestMetadata("stringInterpolationEvaluationOrder.kt")
public void testStringInterpolationEvaluationOrder() throws Exception {
runTest("js/js.translator/testData/box/expression/misc/stringInterpolationEvaluationOrder.kt");
@@ -2631,6 +2631,11 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
runTest("js/js.translator/testData/box/expression/misc/safeCallComputesExpressionOnlyOnce.kt");
}
@TestMetadata("stackTraceCapturing.kt")
public void testStackTraceCapturing() throws Exception {
runTest("js/js.translator/testData/box/expression/misc/stackTraceCapturing.kt");
}
@TestMetadata("stringInterpolationEvaluationOrder.kt")
public void testStringInterpolationEvaluationOrder() throws Exception {
runTest("js/js.translator/testData/box/expression/misc/stringInterpolationEvaluationOrder.kt");
@@ -0,0 +1,61 @@
// EXPECTED_REACHABLE_NODES: 1281
// KJS_WITH_FULL_RUNTIME
// IGNORE_BACKEND: JS
// Reproduction of KT-37563
// Test stack trace capturing in various kinds of constructors
public open class MyExceptionPrimary(message: String) : Exception(message, null) {
constructor() : this("empty")
}
public open class MyExceptionSecondary : MyExceptionPrimary {
constructor(message: String) : super(message)
}
@JsName("foo__0") // Need a stable name to test stack trace text.
fun foo__0() {
throw Exception("msg", null)
}
@JsName("foo__1")
fun foo__1() {
throw MyExceptionPrimary("primary")
}
@JsName("foo__2")
fun foo__2(): Throwable {
throw MyExceptionPrimary()
}
@JsName("foo__3")
fun foo__3(): Throwable {
throw MyExceptionSecondary("secondaryOnly")
}
fun box(): String {
val functions = listOf(
::foo__0,
::foo__1,
::foo__2,
::foo__3
)
var count = 0
for ((i, f) in functions.withIndex()) {
try {
f()
} catch (e: Throwable) {
count++
val stack = e.asDynamic().stack as String
// Even though stack trace format is not stadard,
// it should contain names of the functions.
if (!stack.contains("foo__$i")) return "fail $i"
}
}
if (count != functions.size) return "fail count"
return "OK"
}
@@ -79,11 +79,9 @@ internal fun getStringHashCode(str: String): Int {
internal fun identityHashCode(obj: Any?): Int = getObjectHashCode(obj)
internal fun captureStack(instance: Throwable) {
internal fun captureStack(instance: Throwable, constructorFunction: Any) {
if (js("Error").captureStackTrace != null) {
// TODO Why we generated get kclass for throwable in original code?
js("Error").captureStackTrace(instance, instance.asDynamic().constructor)
// js("Error").captureStackTrace(instance, instance::class.js)
js("Error").captureStackTrace(instance, constructorFunction)
} else {
instance.asDynamic().stack = js("new Error()").stack
}
@@ -110,7 +108,6 @@ internal fun setPropertiesToThrowableInstance(this_: dynamic, message: String?,
this_.cause = cause
}
this_.name = JsObject.getPrototypeOf(this_).constructor.name
captureStack(this_)
}
@JsName("Object")