Move out JVM debugger functionality

This commit is contained in:
Yan Zhulanow
2019-04-05 15:10:14 +03:00
parent 5843336d42
commit ae7550c5af
318 changed files with 1068 additions and 723 deletions
+2
View File
@@ -0,0 +1,2 @@
eval4j is a Java byte code interpreter written in Kotlin.
Its primary use case is implementing expression evaluation in debuggers.
+24
View File
@@ -0,0 +1,24 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
compile(kotlinStdlib())
compile(project(":compiler:backend"))
compile(files(toolsJar()))
compileOnly(intellijDep()) { includeJars("asm-all", rootProject = rootProject) }
testCompile(project(":kotlin-test:kotlin-test-junit"))
testCompile(commonDep("junit:junit"))
testCompile(intellijDep()) { includeJars("asm-all", rootProject = rootProject) }
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
projectTest(parallel = true) {
dependsOn(":dist")
workingDir = rootDir
}
@@ -0,0 +1,425 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j
import org.jetbrains.org.objectweb.asm.Handle
import org.jetbrains.org.objectweb.asm.Opcodes.*
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException
import org.jetbrains.org.objectweb.asm.tree.analysis.Interpreter
class UnsupportedByteCodeException(message: String) : RuntimeException(message)
interface Eval {
fun loadClass(classType: Type): Value
fun loadString(str: String): Value
fun newInstance(classType: Type): Value
fun isInstanceOf(value: Value, targetType: Type): Boolean
fun newArray(arrayType: Type, size: Int): Value
fun newMultiDimensionalArray(arrayType: Type, dimensionSizes: List<Int>): Value
fun getArrayLength(array: Value): Value
fun getArrayElement(array: Value, index: Value): Value
fun setArrayElement(array: Value, index: Value, newValue: Value)
fun getStaticField(fieldDesc: FieldDescription): Value
fun setStaticField(fieldDesc: FieldDescription, newValue: Value)
fun invokeStaticMethod(methodDesc: MethodDescription, arguments: List<Value>): Value
fun getField(instance: Value, fieldDesc: FieldDescription): Value
fun setField(instance: Value, fieldDesc: FieldDescription, newValue: Value)
fun invokeMethod(instance: Value, methodDesc: MethodDescription, arguments: List<Value>, invokespecial: Boolean = false): Value
}
class SingleInstructionInterpreter(private val eval: Eval) : Interpreter<Value>(API_VERSION) {
override fun newValue(type: Type?): Value? {
if (type == null) {
return NOT_A_VALUE
}
return makeNotInitializedValue(type)
}
override fun newOperation(insn: AbstractInsnNode): Value? {
return when (insn.opcode) {
ACONST_NULL -> {
return NULL_VALUE
}
ICONST_M1 -> int(-1)
ICONST_0 -> int(0)
ICONST_1 -> int(1)
ICONST_2 -> int(2)
ICONST_3 -> int(3)
ICONST_4 -> int(4)
ICONST_5 -> int(5)
LCONST_0 -> long(0)
LCONST_1 -> long(1)
FCONST_0 -> float(0.0f)
FCONST_1 -> float(1.0f)
FCONST_2 -> float(2.0f)
DCONST_0 -> double(0.0)
DCONST_1 -> double(1.0)
BIPUSH, SIPUSH -> int((insn as IntInsnNode).operand)
LDC -> {
val cst = ((insn as LdcInsnNode)).cst
when (cst) {
is Int -> int(cst)
is Float -> float(cst)
is Long -> long(cst)
is Double -> double(cst)
is String -> eval.loadString(cst)
is Type -> {
val sort = cst.sort
when (sort) {
Type.OBJECT, Type.ARRAY -> eval.loadClass(cst)
Type.METHOD -> throw UnsupportedByteCodeException("Method handles are not supported")
else -> throw UnsupportedByteCodeException("Illegal LDC constant " + cst)
}
}
is Handle -> throw UnsupportedByteCodeException("Method handles are not supported")
else -> throw UnsupportedByteCodeException("Illegal LDC constant " + cst)
}
}
JSR -> LabelValue((insn as JumpInsnNode).label)
GETSTATIC -> eval.getStaticField(FieldDescription(insn as FieldInsnNode))
NEW -> eval.newInstance(Type.getObjectType((insn as TypeInsnNode).desc))
else -> throw UnsupportedByteCodeException("$insn")
}
}
override fun copyOperation(insn: AbstractInsnNode, value: Value): Value {
return value
}
override fun unaryOperation(insn: AbstractInsnNode, value: Value): Value? {
return when (insn.opcode) {
INEG -> int(-value.int)
IINC -> int(value.int + (insn as IincInsnNode).incr)
L2I -> int(value.long.toInt())
F2I -> int(value.float.toInt())
D2I -> int(value.double.toInt())
I2B -> byte(value.int.toByte())
I2C -> char(value.int.toChar())
I2S -> short(value.int.toShort())
FNEG -> float(-value.float)
I2F -> float(value.int.toFloat())
L2F -> float(value.long.toFloat())
D2F -> float(value.double.toFloat())
LNEG -> long(-value.long)
I2L -> long(value.int.toLong())
F2L -> long(value.float.toLong())
D2L -> long(value.double.toLong())
DNEG -> double(-value.double)
I2D -> double(value.int.toDouble())
L2D -> double(value.long.toDouble())
F2D -> double(value.float.toDouble())
IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IFNULL, IFNONNULL -> {
// Handled by interpreter loop, see checkUnaryCondition()
null
}
// TODO: switch
TABLESWITCH,
LOOKUPSWITCH -> throw UnsupportedByteCodeException("Switch is not supported yet")
PUTSTATIC -> {
eval.setStaticField(FieldDescription(insn as FieldInsnNode), value)
null
}
GETFIELD -> eval.getField(value, FieldDescription(insn as FieldInsnNode))
NEWARRAY -> {
val typeStr = when ((insn as IntInsnNode).operand) {
T_BOOLEAN -> "[Z"
T_CHAR -> "[C"
T_BYTE -> "[B"
T_SHORT -> "[S"
T_INT -> "[I"
T_FLOAT -> "[F"
T_DOUBLE -> "[D"
T_LONG -> "[J"
else -> throw AnalyzerException(insn, "Invalid array type")
}
eval.newArray(Type.getType(typeStr), value.int)
}
ANEWARRAY -> {
val desc = (insn as TypeInsnNode).desc
eval.newArray(Type.getType("[" + Type.getObjectType(desc)), value.int)
}
ARRAYLENGTH -> eval.getArrayLength(value)
ATHROW -> {
// Handled by interpreter loop
null
}
CHECKCAST -> {
val targetType = Type.getObjectType((insn as TypeInsnNode).desc)
when {
value == NULL_VALUE -> NULL_VALUE
eval.isInstanceOf(value, targetType) -> ObjectValue(value.obj(), targetType)
else -> throwInterpretingException(ClassCastException("${value.asmType.className} cannot be cast to ${targetType.className}"))
}
}
INSTANCEOF -> {
val targetType = Type.getObjectType((insn as TypeInsnNode).desc)
boolean(eval.isInstanceOf(value, targetType))
}
// TODO: maybe just do nothing?
MONITORENTER, MONITOREXIT -> throw UnsupportedByteCodeException("Monitor instructions are not supported")
else -> throw UnsupportedByteCodeException("$insn")
}
}
fun checkUnaryCondition(value: Value, opcode: Int): Boolean {
return when (opcode) {
IFEQ -> value.int == 0
IFNE -> value.int != 0
IFLT -> value.int < 0
IFGT -> value.int > 0
IFLE -> value.int <= 0
IFGE -> value.int >= 0
IFNULL -> value.obj() == null
IFNONNULL -> value.obj() != null
else -> throw UnsupportedByteCodeException("Unknown opcode: $opcode")
}
}
private fun divisionByZero(): Nothing = throwInterpretingException(ArithmeticException("Division by zero"))
override fun binaryOperation(insn: AbstractInsnNode, value1: Value, value2: Value): Value? {
return when (insn.opcode) {
IALOAD, BALOAD, CALOAD, SALOAD,
FALOAD, LALOAD, DALOAD,
AALOAD -> eval.getArrayElement(value1, value2)
IADD -> int(value1.int + value2.int)
ISUB -> int(value1.int - value2.int)
IMUL -> int(value1.int * value2.int)
IDIV -> {
val divider = value2.int
if (divider == 0) {
divisionByZero()
}
int(value1.int / divider)
}
IREM -> {
val divider = value2.int
if (divider == 0) {
divisionByZero()
}
int(value1.int % divider)
}
ISHL -> int(value1.int shl value2.int)
ISHR -> int(value1.int shr value2.int)
IUSHR -> int(value1.int ushr value2.int)
IAND -> int(value1.int and value2.int)
IOR -> int(value1.int or value2.int)
IXOR -> int(value1.int xor value2.int)
LADD -> long(value1.long + value2.long)
LSUB -> long(value1.long - value2.long)
LMUL -> long(value1.long * value2.long)
LDIV -> {
val divider = value2.long
if (divider == 0L) {
divisionByZero()
}
long(value1.long / divider)
}
LREM -> {
val divider = value2.long
if (divider == 0L) {
divisionByZero()
}
long(value1.long % divider)
}
LSHL -> long(value1.long shl value2.int)
LSHR -> long(value1.long shr value2.int)
LUSHR -> long(value1.long ushr value2.int)
LAND -> long(value1.long and value2.long)
LOR -> long(value1.long or value2.long)
LXOR -> long(value1.long xor value2.long)
FADD -> float(value1.float + value2.float)
FSUB -> float(value1.float - value2.float)
FMUL -> float(value1.float * value2.float)
FDIV -> {
val divider = value2.float
if (divider == 0f) {
divisionByZero()
}
float(value1.float / divider)
}
FREM -> {
val divider = value2.float
if (divider == 0f) {
divisionByZero()
}
float(value1.float % divider)
}
DADD -> double(value1.double + value2.double)
DSUB -> double(value1.double - value2.double)
DMUL -> double(value1.double * value2.double)
DDIV -> {
val divider = value2.double
if (divider == 0.0) {
divisionByZero()
}
double(value1.double / divider)
}
DREM -> {
val divider = value2.double
if (divider == 0.0) {
divisionByZero()
}
double(value1.double % divider)
}
LCMP -> {
val l1 = value1.long
val l2 = value2.long
int(when {
l1 > l2 -> 1
l1 == l2 -> 0
else -> -1
})
}
FCMPL,
FCMPG -> {
val l1 = value1.float
val l2 = value2.float
int(when {
l1 > l2 -> 1
l1 == l2 -> 0
l1 < l2 -> -1
// one of them is NaN
else -> if (insn.opcode == FCMPG) 1 else -1
})
}
DCMPL,
DCMPG -> {
val l1 = value1.double
val l2 = value2.double
int(when {
l1 > l2 -> 1
l1 == l2 -> 0
l1 < l2 -> -1
// one of them is NaN
else -> if (insn.opcode == DCMPG) 1 else -1
})
}
IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE -> {
// Handled by interpreter loop, see checkBinaryCondition()
null
}
PUTFIELD -> {
eval.setField(value1, FieldDescription(insn as FieldInsnNode), value2)
null
}
else -> throw UnsupportedByteCodeException("$insn")
}
}
fun checkBinaryCondition(value1: Value, value2: Value, opcode: Int): Boolean {
return when (opcode) {
IF_ICMPEQ -> value1.int == value2.int
IF_ICMPNE -> value1.int != value2.int
IF_ICMPLT -> value1.int < value2.int
IF_ICMPGT -> value1.int > value2.int
IF_ICMPLE -> value1.int <= value2.int
IF_ICMPGE -> value1.int >= value2.int
IF_ACMPEQ -> value1.obj() == value2.obj()
IF_ACMPNE -> value1.obj() != value2.obj()
else -> throw UnsupportedByteCodeException("Unknown opcode: $opcode")
}
}
override fun ternaryOperation(insn: AbstractInsnNode, value1: Value, value2: Value, value3: Value): Value? {
return when (insn.opcode) {
IASTORE, LASTORE, FASTORE, DASTORE, AASTORE, BASTORE, CASTORE, SASTORE -> {
eval.setArrayElement(value1, value2, value3)
null
}
else -> throw UnsupportedByteCodeException("$insn")
}
}
override fun naryOperation(insn: AbstractInsnNode, values: List<Value>): Value {
return when (insn.opcode) {
MULTIANEWARRAY -> {
val node = insn as MultiANewArrayInsnNode
eval.newMultiDimensionalArray(Type.getType(node.desc), values.map(Value::int))
}
INVOKEVIRTUAL, INVOKESPECIAL, INVOKEINTERFACE -> {
eval.invokeMethod(
values[0],
MethodDescription(insn as MethodInsnNode),
values.subList(1, values.size),
insn.opcode == INVOKESPECIAL
)
}
INVOKESTATIC -> eval.invokeStaticMethod(MethodDescription(insn as MethodInsnNode), values)
INVOKEDYNAMIC -> throw UnsupportedByteCodeException("INVOKEDYNAMIC is not supported")
else -> throw UnsupportedByteCodeException("$insn")
}
}
override fun returnOperation(insn: AbstractInsnNode, value: Value, expected: Value) {
when (insn.opcode) {
IRETURN, LRETURN, FRETURN, DRETURN, ARETURN -> {
// Handled by interpreter loop
}
else -> throw UnsupportedByteCodeException("$insn")
}
}
override fun merge(v: Value, w: Value): Value {
// We always remember the NEW value
return w
}
}
@@ -0,0 +1,274 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j
import org.jetbrains.eval4j.ExceptionThrown.ExceptionKind
import org.jetbrains.org.objectweb.asm.Opcodes.*
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
import org.jetbrains.org.objectweb.asm.util.Printer
import java.util.*
interface InterpreterResult {
override fun toString(): String
}
class ExceptionThrown(val exception: ObjectValue, val kind: ExceptionKind): InterpreterResult {
override fun toString(): String = "Thrown $exception: $kind"
enum class ExceptionKind {
FROM_EVALUATED_CODE,
FROM_EVALUATOR,
BROKEN_CODE
}
}
data class ValueReturned(val result: Value): InterpreterResult {
override fun toString(): String = "Returned $result"
}
class AbnormalTermination(val message: String): InterpreterResult {
override fun toString(): String = "Terminated abnormally: $message"
}
interface InterpretationEventHandler {
object NONE : InterpretationEventHandler {
override fun instructionProcessed(insn: AbstractInsnNode): InterpreterResult? = null
override fun exceptionThrown(currentState: Frame<Value>, currentInsn: AbstractInsnNode, exception: Value): InterpreterResult? = null
override fun exceptionCaught(currentState: Frame<Value>, currentInsn: AbstractInsnNode, exception: Value): InterpreterResult? = null
}
// If a non-null value is returned, interpreter loop is terminated and that value is used as a result
fun instructionProcessed(insn: AbstractInsnNode): InterpreterResult?
fun exceptionThrown(currentState: Frame<Value>, currentInsn: AbstractInsnNode, exception: Value): InterpreterResult?
fun exceptionCaught(currentState: Frame<Value>, currentInsn: AbstractInsnNode, exception: Value): InterpreterResult?
}
abstract class ThrownFromEvalExceptionBase(cause: Throwable): RuntimeException(cause) {
override fun toString(): String = "Thrown by evaluator: ${cause}"
}
class BrokenCode(cause: Throwable) : ThrownFromEvalExceptionBase(cause)
// Interpreting exceptions should not be sent to EA
class Eval4JInterpretingException(override val cause: Throwable) : RuntimeException(cause)
class ThrownFromEvaluatedCodeException(val exception: ObjectValue): RuntimeException() {
override fun toString(): String = "Thrown from evaluated code: $exception"
}
fun interpreterLoop(
m: MethodNode,
initialState: Frame<Value>,
eval: Eval,
handler: InterpretationEventHandler = InterpretationEventHandler.NONE
): InterpreterResult {
val firstInsn = m.instructions.first
if (firstInsn == null) throw IllegalArgumentException("Empty method")
var currentInsn = firstInsn
fun goto(nextInsn: AbstractInsnNode?) {
if (nextInsn == null) throw IllegalArgumentException("Instruction flow ended with no RETURN")
currentInsn = nextInsn
}
val interpreter = SingleInstructionInterpreter(eval)
val frame = Frame(initialState)
val handlers = computeHandlers(m)
class ResultException(val result: InterpreterResult): RuntimeException()
fun exceptionCaught(exceptionValue: Value, instanceOf: (Type) -> Boolean): Boolean {
val catchBlocks = handlers[m.instructions.indexOf(currentInsn)] ?: listOf()
for (catch in catchBlocks) {
val exceptionTypeInternalName = catch.type
if (exceptionTypeInternalName != null) {
val exceptionType = Type.getObjectType(exceptionTypeInternalName)
if (instanceOf(exceptionType)) {
val handled = handler.exceptionCaught(frame, currentInsn, exceptionValue)
if (handled != null) throw ResultException(handled)
frame.clearStack()
frame.push(exceptionValue)
goto(catch.handler)
return true
}
}
}
return false
}
fun exceptionCaught(exceptionValue: Value): Boolean = exceptionCaught(exceptionValue) {
exceptionType -> eval.isInstanceOf(exceptionValue, exceptionType)
}
fun exceptionFromEvalCaught(exception: Throwable, exceptionValue: Value): Boolean {
return exceptionCaught(exceptionValue) {
exceptionType ->
try {
val exceptionClass = exception::class.java
val _class = Class.forName(
exceptionType.internalName.replace('/', '.'),
true,
exceptionClass.classLoader
)
_class.isAssignableFrom(exceptionClass)
}
catch (e: ClassNotFoundException) {
// If the class is not available in this VM, it can not be a superclass of an exception trown in it
false
}
}
}
try {
loop@ while (true) {
val insnOpcode = currentInsn.opcode
val insnType = currentInsn.type
when (insnType) {
AbstractInsnNode.LABEL,
AbstractInsnNode.FRAME,
AbstractInsnNode.LINE -> {
// skip to the next instruction
}
else -> {
when (insnOpcode) {
GOTO -> {
goto((currentInsn as JumpInsnNode).label)
continue@loop
}
RET -> {
val varNode = currentInsn as VarInsnNode
val address = frame.getLocal(varNode.`var`)
goto((address as LabelValue).value)
continue@loop
}
// TODO: switch
LOOKUPSWITCH -> UnsupportedByteCodeException("LOOKUPSWITCH is not supported yet")
TABLESWITCH -> UnsupportedByteCodeException("TABLESWITCH is not supported yet")
IRETURN, LRETURN, FRETURN, DRETURN, ARETURN -> {
val value = frame.getStackTop()
val expectedType = Type.getReturnType(m.desc)
if (expectedType.sort == Type.OBJECT || expectedType.sort == Type.ARRAY) {
val coerced = if (value != NULL_VALUE && value.asmType != expectedType)
ObjectValue(value.obj(), expectedType)
else value
return ValueReturned(coerced)
}
if (value.asmType != expectedType) {
assert(insnOpcode == IRETURN) { "Only ints should be coerced: ${Printer.OPCODES[insnOpcode]}" }
val coerced = when (expectedType.sort) {
Type.BOOLEAN -> boolean(value.boolean)
Type.BYTE -> byte(value.int.toByte())
Type.SHORT -> short(value.int.toShort())
Type.CHAR -> char(value.int.toChar())
Type.INT -> int(value.int)
else -> throw UnsupportedByteCodeException("Should not be coerced: $expectedType")
}
return ValueReturned(coerced)
}
return ValueReturned(value)
}
RETURN -> return ValueReturned(VOID_VALUE)
IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IFNULL, IFNONNULL -> {
if (interpreter.checkUnaryCondition(frame.getStackTop(), insnOpcode)) {
frame.execute(currentInsn, interpreter)
goto((currentInsn as JumpInsnNode).label)
continue@loop
}
}
IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE -> {
if (interpreter.checkBinaryCondition(frame.getStackTop(1), frame.getStackTop(0), insnOpcode)) {
frame.execute(currentInsn, interpreter)
goto((currentInsn as JumpInsnNode).label)
continue@loop
}
}
ATHROW -> {
val exceptionValue = frame.getStackTop() as ObjectValue
val handled = handler.exceptionThrown(frame, currentInsn, exceptionValue)
if (handled != null) return handled
if (exceptionCaught(exceptionValue)) continue@loop
return ExceptionThrown(exceptionValue, ExceptionKind.FROM_EVALUATED_CODE)
}
// Workaround for a bug in Kotlin: NoPatterMatched exception is thrown otherwise!
else -> {}
}
try {
frame.execute(currentInsn, interpreter)
}
catch (e: ThrownFromEvalExceptionBase) {
val exception = e.cause!!
val exceptionValue = ObjectValue(exception, Type.getType(exception::class.java))
val handled = handler.exceptionThrown(frame, currentInsn,
exceptionValue)
if (handled != null) return handled
if (exceptionFromEvalCaught(exception, exceptionValue)) continue@loop
val exceptionType = if (e is BrokenCode) ExceptionKind.BROKEN_CODE else ExceptionKind.FROM_EVALUATOR
return ExceptionThrown(exceptionValue, exceptionType)
}
catch (e: ThrownFromEvaluatedCodeException) {
val handled = handler.exceptionThrown(frame, currentInsn, e.exception)
if (handled != null) return handled
if (exceptionCaught(e.exception)) continue@loop
return ExceptionThrown(e.exception, ExceptionKind.FROM_EVALUATED_CODE)
}
}
}
val handled = handler.instructionProcessed(currentInsn)
if (handled != null) return handled
goto(currentInsn.next)
}
}
catch(e: ResultException) {
return e.result
}
}
private fun <T: Value> Frame<T>.getStackTop(i: Int = 0) = this.getStack(this.stackSize - 1 - i) ?: throwBrokenCodeException(IllegalArgumentException("Couldn't get value with index = $i from top of stack"))
// Copied from org.jetbrains.org.objectweb.asm.tree.analysis.Analyzer.analyze()
fun computeHandlers(m: MethodNode): Array<out List<TryCatchBlockNode>?> {
val insns = m.instructions
val handlers = Array<MutableList<TryCatchBlockNode>?>(insns.size()) {null}
for (tcb in m.tryCatchBlocks) {
val begin = insns.indexOf(tcb.start)
val end = insns.indexOf(tcb.end)
for (i in begin..end - 1) {
val insnHandlers = handlers[i] ?: ArrayList<TryCatchBlockNode>()
handlers[i] = insnHandlers
insnHandlers.add(tcb)
}
}
return handlers
}
@@ -0,0 +1,518 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.jdi
import com.intellij.openapi.util.text.StringUtil
import com.sun.jdi.*
import org.jetbrains.eval4j.*
import org.jetbrains.eval4j.Value
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper.InternalNameMapper.canBeMangledInternalName
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper.InternalNameMapper.internalNameWithoutModuleSuffix
import org.jetbrains.org.objectweb.asm.Type
import java.lang.reflect.AccessibleObject
import com.sun.jdi.Type as jdi_Type
import com.sun.jdi.Value as jdi_Value
private val CLASS = Type.getType(Class::class.java)
private val OBJECT = Type.getType(Any::class.java)
private val BOOTSTRAP_CLASS_DESCRIPTORS = setOf("Ljava/lang/String;", "Ljava/lang/ClassLoader;", "Ljava/lang/Class;")
class JDIEval(
private val vm: VirtualMachine,
private val defaultClassLoader: ClassLoaderReference?,
private val thread: ThreadReference,
private val invokePolicy: Int
) : Eval {
private val primitiveTypes = mapOf(
Type.BOOLEAN_TYPE.className to vm.mirrorOf(true).type(),
Type.BYTE_TYPE.className to vm.mirrorOf(1.toByte()).type(),
Type.SHORT_TYPE.className to vm.mirrorOf(1.toShort()).type(),
Type.INT_TYPE.className to vm.mirrorOf(1).type(),
Type.CHAR_TYPE.className to vm.mirrorOf('1').type(),
Type.LONG_TYPE.className to vm.mirrorOf(1L).type(),
Type.FLOAT_TYPE.className to vm.mirrorOf(1.0f).type(),
Type.DOUBLE_TYPE.className to vm.mirrorOf(1.0).type()
)
private val isJava8OrLater = StringUtil.compareVersionNumbers(vm.version(), "1.8") >= 0
override fun loadClass(classType: Type): Value {
return loadClass(classType, defaultClassLoader)
}
fun loadClass(classType: Type, classLoader: ClassLoaderReference?): Value {
val loadedClasses = vm.classesByName(classType.internalName)
if (!loadedClasses.isEmpty()) {
for (loadedClass in loadedClasses) {
if (loadedClass.isPrepared && (classType.descriptor in BOOTSTRAP_CLASS_DESCRIPTORS || loadedClass.classLoader() == classLoader)) {
return loadedClass.classObject().asValue()
}
}
}
if (classLoader == null) {
return invokeStaticMethod(
MethodDescription(
CLASS.internalName,
"forName",
"(Ljava/lang/String;)Ljava/lang/Class;",
true
),
listOf(vm.mirrorOf(classType.internalName.replace('/', '.')).asValue())
)
}
else {
return invokeStaticMethod(
MethodDescription(
CLASS.internalName,
"forName",
"(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;",
true
),
listOf(
vm.mirrorOf(classType.internalName.replace('/', '.')).asValue(),
boolean(true),
classLoader.asValue()
)
)
}
}
fun loadClassByName(name: String, classLoader: ClassLoaderReference): jdi_Type {
val dimensions = name.count { it == '[' }
val baseTypeName = if (dimensions > 0) name.substring(0, name.indexOf('[')) else name
val baseType = primitiveTypes[baseTypeName] ?: Type.getType("L$baseTypeName;").asReferenceType(classLoader)
return if (dimensions == 0)
baseType
else
Type.getType("[".repeat(dimensions) + baseType.asType().descriptor).asReferenceType(classLoader)
}
override fun loadString(str: String): Value = vm.mirrorOf(str).asValue()
override fun newInstance(classType: Type): Value {
return NewObjectValue(classType)
}
override fun isInstanceOf(value: Value, targetType: Type): Boolean {
assert(targetType.sort == Type.OBJECT || targetType.sort == Type.ARRAY) {
"Can't check isInstanceOf() for non-object type $targetType"
}
val _class = loadClass(targetType)
return invokeMethod(
_class,
MethodDescription(
CLASS.internalName,
"isInstance",
"(Ljava/lang/Object;)Z",
false
),
listOf(value)).boolean
}
fun Type.asReferenceType(classLoader: ClassLoaderReference? = this@JDIEval.defaultClassLoader): ReferenceType = loadClass(this, classLoader).jdiClass!!.reflectedType()
fun Type.asArrayType(classLoader: ClassLoaderReference? = this@JDIEval.defaultClassLoader): ArrayType = asReferenceType(classLoader) as ArrayType
override fun newArray(arrayType: Type, size: Int): Value {
val jdiArrayType = arrayType.asArrayType()
return jdiArrayType.newInstance(size).asValue()
}
private val Type.arrayElementType: Type
get(): Type {
assert(sort == Type.ARRAY) { "Not an array type: $this" }
return Type.getType(descriptor.substring(1))
}
private fun fillArray(elementType: Type, size: Int, nestedSizes: List<Int>): Value {
val arr = newArray(Type.getType("[" + elementType.descriptor), size)
if (!nestedSizes.isEmpty()) {
val nestedElementType = elementType.arrayElementType
val nestedSize = nestedSizes[0]
val tail = nestedSizes.drop(1)
for (i in 0..size - 1) {
setArrayElement(arr, int(i), fillArray(nestedElementType, nestedSize, tail))
}
}
return arr
}
override fun newMultiDimensionalArray(arrayType: Type, dimensionSizes: List<Int>): Value {
return fillArray(arrayType.arrayElementType, dimensionSizes[0], dimensionSizes.drop(1))
}
private fun Value.array() = jdiObj.checkNull() as ArrayReference
override fun getArrayLength(array: Value): Value {
return int(array.array().length())
}
override fun getArrayElement(array: Value, index: Value): Value {
try {
return array.array().getValue(index.int).asValue()
}
catch (e: IndexOutOfBoundsException) {
throwInterpretingException(ArrayIndexOutOfBoundsException(e.message))
}
}
override fun setArrayElement(array: Value, index: Value, newValue: Value) {
try {
return array.array().setValue(index.int, newValue.asJdiValue(vm, array.asmType.arrayElementType))
}
catch (e: IndexOutOfBoundsException) {
throwInterpretingException(ArrayIndexOutOfBoundsException(e.message))
}
}
private fun findField(fieldDesc: FieldDescription, receiver: ReferenceType? = null): Field {
for (owner in listOfNotNull(receiver, fieldDesc.ownerType.asReferenceType())) {
owner.fieldByName(fieldDesc.name)?.let { return it }
}
throwBrokenCodeException(NoSuchFieldError("Field not found: $fieldDesc"))
}
private fun findStaticField(fieldDesc: FieldDescription): Field {
val field = findField(fieldDesc)
if (!field.isStatic) {
throwBrokenCodeException(NoSuchFieldError("Field is not static: $fieldDesc"))
}
return field
}
override fun getStaticField(fieldDesc: FieldDescription): Value {
val field = findStaticField(fieldDesc)
return mayThrow { field.declaringType().getValue(field) }.ifFail(field).asValue()
}
override fun setStaticField(fieldDesc: FieldDescription, newValue: Value) {
val field = findStaticField(fieldDesc)
if (field.isFinal) {
throwBrokenCodeException(NoSuchFieldError("Can't modify a final field: $field"))
}
val _class = field.declaringType()
if (_class !is ClassType) {
throwBrokenCodeException(NoSuchFieldError("Can't a field in a non-class: $field"))
}
val jdiValue = newValue.asJdiValue(vm, field.type().asType())
mayThrow { _class.setValue(field, jdiValue) }.ifFail(field)
}
private fun findMethod(methodDesc: MethodDescription, _class: ReferenceType = methodDesc.ownerType.asReferenceType()): Method {
val methodName = methodDesc.name
val method = when (_class) {
is ClassType ->
_class.concreteMethodByName(methodName, methodDesc.desc)
else ->
_class.methodsByName(methodName, methodDesc.desc).firstOrNull()
}
if (method != null) {
return method
}
// Module name can be different for internal functions during evaluation and compilation
val internalNameWithoutSuffix = internalNameWithoutModuleSuffix(methodName)
if (internalNameWithoutSuffix != null) {
val internalMethods = _class.visibleMethods().filter {
val name = it.name()
name.startsWith(internalNameWithoutSuffix) && canBeMangledInternalName(name) && it.signature() == methodDesc.desc
}
if (!internalMethods.isEmpty()) {
return internalMethods.singleOrNull() ?:
throwBrokenCodeException(IllegalArgumentException("Several internal methods found for $methodDesc"))
}
}
throwBrokenCodeException(NoSuchMethodError("Method not found: $methodDesc"))
}
override fun invokeStaticMethod(methodDesc: MethodDescription, arguments: List<Value>): Value {
val method = findMethod(methodDesc)
if (!method.isStatic) {
throwBrokenCodeException(NoSuchMethodError("Method is not static: $methodDesc"))
}
val declaringType = method.declaringType()
val args = mapArguments(arguments, method.safeArgumentTypes())
if (shouldInvokeMethodWithReflection(method, args)) {
return invokeMethodWithReflection(declaringType.asType(), NULL_VALUE, args, methodDesc)
}
args.disableCollection()
val result = mayThrow {
when (declaringType) {
is ClassType -> declaringType.invokeMethod(thread, method, args, invokePolicy)
is InterfaceType -> {
if (!isJava8OrLater) {
val message = "Calling interface static methods is not supported in JVM ${vm.version()} ($method)"
throwBrokenCodeException(NoSuchMethodError(message))
}
declaringType.invokeMethod(thread, method, args, invokePolicy)
}
else -> {
val message = "Calling static methods is only supported for classes and interfaces ($method)"
throwBrokenCodeException(NoSuchMethodError(message))
}
}
}.ifFail(method)
args.enableCollection()
return result.asValue()
}
override fun getField(instance: Value, fieldDesc: FieldDescription): Value {
val receiver = instance.jdiObj.checkNull()
val field = findField(fieldDesc, receiver.referenceType())
return mayThrow {
try {
receiver.getValue(field)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException("Possibly incompatible types: " +
"field declaring type = ${field.declaringType()}, " +
"instance type = ${receiver.referenceType()}")
}
}.ifFail(field, receiver).asValue()
}
override fun setField(instance: Value, fieldDesc: FieldDescription, newValue: Value) {
val receiver = instance.jdiObj.checkNull()
val field = findField(fieldDesc, receiver.referenceType())
val jdiValue = newValue.asJdiValue(vm, field.type().asType())
mayThrow { receiver.setValue(field, jdiValue) }
}
fun unboxType(boxedValue: Value, type: Type): Value {
val method = when (type) {
Type.INT_TYPE -> MethodDescription("java/lang/Integer", "intValue", "()I", false)
Type.BOOLEAN_TYPE -> MethodDescription("java/lang/Boolean", "booleanValue", "()Z", false)
Type.CHAR_TYPE -> MethodDescription("java/lang/Character", "charValue", "()C", false)
Type.SHORT_TYPE -> MethodDescription("java/lang/Character", "shortValue", "()S", false)
Type.LONG_TYPE -> MethodDescription("java/lang/Long", "longValue", "()J", false)
Type.BYTE_TYPE -> MethodDescription("java/lang/Byte", "byteValue", "()B", false)
Type.FLOAT_TYPE -> MethodDescription("java/lang/Float", "floatValue", "()F", false)
Type.DOUBLE_TYPE -> MethodDescription("java/lang/Double", "doubleValue", "()D", false)
else -> throw UnsupportedOperationException("Couldn't unbox non primitive type ${type.internalName}")
}
return invokeMethod(boxedValue, method, listOf(), true)
}
fun boxType(value: Value): Value {
val method = when (value.asmType) {
Type.INT_TYPE -> MethodDescription("java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false)
Type.BYTE_TYPE -> MethodDescription("java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false)
Type.SHORT_TYPE -> MethodDescription("java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false)
Type.LONG_TYPE -> MethodDescription("java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false)
Type.BOOLEAN_TYPE -> MethodDescription("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false)
Type.CHAR_TYPE -> MethodDescription("java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false)
Type.FLOAT_TYPE -> MethodDescription("java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false)
Type.DOUBLE_TYPE -> MethodDescription("java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false)
else -> throw UnsupportedOperationException("Couldn't box non-primitive type ${value.asmType.internalName}")
}
return invokeStaticMethod(method, listOf(value))
}
override fun invokeMethod(instance: Value, methodDesc: MethodDescription, arguments: List<Value>, invokespecial: Boolean): Value {
if (invokespecial && methodDesc.name == "<init>") {
// Constructor call
val ctor = findMethod(methodDesc)
val _class = (instance as NewObjectValue).asmType.asReferenceType() as ClassType
val args = mapArguments(arguments, ctor.safeArgumentTypes())
args.disableCollection()
val result = mayThrow { _class.newInstance(thread, ctor, args, invokePolicy) }.ifFail(ctor)
args.enableCollection()
instance.value = result
return result.asValue()
}
fun doInvokeMethod(obj: ObjectReference, method: Method, policy: Int): Value {
val args = mapArguments(arguments, method.safeArgumentTypes())
if (shouldInvokeMethodWithReflection(method, args)) {
return invokeMethodWithReflection(instance.asmType, instance, args, methodDesc)
}
args.disableCollection()
val result = mayThrow { obj.invokeMethod(thread, method, args, policy) }.ifFail(method, obj)
args.enableCollection()
return result.asValue()
}
val obj = instance.jdiObj.checkNull()
return if (invokespecial) {
val method = findMethod(methodDesc)
doInvokeMethod(obj, method, invokePolicy or ObjectReference.INVOKE_NONVIRTUAL)
}
else {
val method = findMethod(methodDesc, obj.referenceType() ?: methodDesc.ownerType.asReferenceType())
doInvokeMethod(obj, method, invokePolicy)
}
}
private fun shouldInvokeMethodWithReflection(method: Method, args: List<com.sun.jdi.Value?>): Boolean {
if (method.isVarArgs) {
return false
}
val argumentTypes = try {
method.argumentTypes()
} catch (e: ClassNotLoadedException) {
return false
}
return args.zip(argumentTypes).any { isArrayOfInterfaces(it.first?.type(), it.second) }
}
private fun isArrayOfInterfaces(valueType: jdi_Type?, expectedType: jdi_Type?): Boolean {
return (valueType as? ArrayType)?.componentType() is InterfaceType && (expectedType as? ArrayType)?.componentType() == OBJECT.asReferenceType()
}
private fun invokeMethodWithReflection(ownerType: Type, instance: Value, args: List<jdi_Value?>, methodDesc: MethodDescription): Value {
val methodToInvoke = invokeMethod(
loadClass(ownerType),
MethodDescription(
CLASS.internalName,
"getDeclaredMethod",
"(Ljava/lang/String;[L${CLASS.internalName};)Ljava/lang/reflect/Method;",
true
),
listOf(vm.mirrorOf(methodDesc.name).asValue(), *methodDesc.parameterTypes.map { loadClass(it) }.toTypedArray())
)
invokeMethod(
methodToInvoke,
MethodDescription(
Type.getType(AccessibleObject::class.java).internalName,
"setAccessible",
"(Z)V",
true
),
listOf(vm.mirrorOf(true).asValue())
)
val invocationResult = invokeMethod(
methodToInvoke,
MethodDescription(
methodToInvoke.asmType.internalName,
"invoke",
"(L${OBJECT.internalName};[L${OBJECT.internalName};)L${OBJECT.internalName};",
true
),
listOf(instance, *args.map { it.asValue() }.toTypedArray())
)
if (methodDesc.returnType.sort != Type.OBJECT && methodDesc.returnType.sort != Type.ARRAY && methodDesc.returnType.sort != Type.VOID) {
return unboxType(invocationResult, methodDesc.returnType)
}
return invocationResult
}
private fun List<jdi_Value?>.disableCollection() {
forEach { (it as? ObjectReference)?.disableCollection() }
}
private fun List<jdi_Value?>.enableCollection() {
forEach { (it as? ObjectReference)?.enableCollection() }
}
private fun mapArguments(arguments: List<Value>, expectedTypes: List<jdi_Type>): List<jdi_Value?> {
return arguments.zip(expectedTypes).map {
val (arg, expectedType) = it
arg.asJdiValue(vm, expectedType.asType())
}
}
private fun Method.safeArgumentTypes(): List<jdi_Type> {
try {
return argumentTypes()
} catch (e: ClassNotLoadedException) {
return argumentTypeNames()!!.map { name ->
val classLoader = declaringType()?.classLoader()
if (classLoader != null) {
return@map loadClassByName(name, classLoader)
}
when (name) {
"void" -> virtualMachine().mirrorOfVoid().type()
"boolean" -> primitiveTypes.getValue(Type.BOOLEAN_TYPE.className)
"byte" -> primitiveTypes.getValue(Type.BYTE_TYPE.className)
"char" -> primitiveTypes.getValue(Type.CHAR_TYPE.className)
"short" -> primitiveTypes.getValue(Type.SHORT_TYPE.className)
"int" -> primitiveTypes.getValue(Type.INT_TYPE.className)
"long" -> primitiveTypes.getValue(Type.LONG_TYPE.className)
"float" -> primitiveTypes.getValue(Type.FLOAT_TYPE.className)
"double" -> primitiveTypes.getValue(Type.DOUBLE_TYPE.className)
else -> virtualMachine().classesByName(name).firstOrNull()
?: throw IllegalStateException("Unknown class $name")
}
}
}
}
}
@Suppress("unused")
private sealed class JdiOperationResult<T> {
class Fail<T>(val cause: Exception): JdiOperationResult<T>()
class OK<T>(val value: T): JdiOperationResult<T>()
}
private fun <T> mayThrow(f: () -> T): JdiOperationResult<T> {
try {
return JdiOperationResult.OK(f())
}
catch (e: IllegalArgumentException) {
return JdiOperationResult.Fail<T>(e)
}
catch (e: InvocationException) {
throw ThrownFromEvaluatedCodeException(e.exception().asValue())
}
}
private fun memberInfo(member: TypeComponent, thisObj: ObjectReference?): String {
return "\nmember = $member\nobjectRef = $thisObj"
}
private fun <T> JdiOperationResult<T>.ifFail(member: TypeComponent, thisObj: ObjectReference? = null): T {
return ifFail { memberInfo(member, thisObj) }
}
private fun <T> JdiOperationResult<T>.ifFail(lazyMessage: () -> String): T {
return when(this) {
is JdiOperationResult.OK -> this.value
is JdiOperationResult.Fail -> {
if (cause is IllegalArgumentException) {
throwBrokenCodeException(IllegalArgumentException(lazyMessage(), this.cause))
}
else {
throwBrokenCodeException(IllegalStateException(lazyMessage(), this.cause))
}
}
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.jdi
import com.sun.jdi.ClassObjectReference
import com.sun.jdi.VirtualMachine
import org.jetbrains.eval4j.*
import org.jetbrains.org.objectweb.asm.Opcodes.ACC_STATIC
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
import com.sun.jdi.BooleanValue as jdi_BooleanValue
import com.sun.jdi.ByteValue as jdi_ByteValue
import com.sun.jdi.CharValue as jdi_CharValue
import com.sun.jdi.DoubleValue as jdi_DoubleValue
import com.sun.jdi.FloatValue as jdi_FloatValue
import com.sun.jdi.IntegerValue as jdi_IntegerValue
import com.sun.jdi.LongValue as jdi_LongValue
import com.sun.jdi.ObjectReference as jdi_ObjectReference
import com.sun.jdi.ShortValue as jdi_ShortValue
import com.sun.jdi.Type as jdi_Type
import com.sun.jdi.Value as jdi_Value
import com.sun.jdi.VoidValue as jdi_VoidValue
fun makeInitialFrame(methodNode: MethodNode, arguments: List<Value>): Frame<Value> {
val isStatic = (methodNode.access and ACC_STATIC) != 0
val params = Type.getArgumentTypes(methodNode.desc)
assert(arguments.size == (if (isStatic) params.size else params.size + 1)) {
"Wrong number of arguments for $methodNode: $arguments"
}
val frame = Frame<Value>(methodNode.maxLocals, methodNode.maxStack)
frame.setReturn(makeNotInitializedValue(Type.getReturnType(methodNode.desc)))
var index = 0
for (arg in arguments) {
frame.setLocal(index++, arg)
if (arg.size == 2) {
frame.setLocal(index++, NOT_A_VALUE)
}
}
while (index < methodNode.maxLocals) {
frame.setLocal(index++, NOT_A_VALUE)
}
return frame
}
class JDIFailureException(message: String?, cause: Throwable? = null): RuntimeException(message, cause)
fun jdi_ObjectReference?.asValue(): ObjectValue {
return when (this) {
null -> NULL_VALUE
else -> ObjectValue(this, type().asType())
}
}
fun jdi_Value?.asValue(): Value {
return when (this) {
null -> NULL_VALUE
is jdi_VoidValue -> VOID_VALUE
is jdi_BooleanValue -> IntValue(intValue(), Type.BOOLEAN_TYPE)
is jdi_ByteValue -> IntValue(intValue(), Type.BYTE_TYPE)
is jdi_ShortValue -> IntValue(intValue(), Type.SHORT_TYPE)
is jdi_CharValue -> IntValue(intValue(), Type.CHAR_TYPE)
is jdi_IntegerValue -> IntValue(intValue(), Type.INT_TYPE)
is jdi_LongValue -> LongValue(longValue())
is jdi_FloatValue -> FloatValue(floatValue())
is jdi_DoubleValue -> DoubleValue(doubleValue())
is jdi_ObjectReference -> this.asValue()
else -> throw JDIFailureException("Unknown value: $this")
}
}
fun jdi_Type.asType(): Type = Type.getType(this.signature())
val Value.jdiObj: jdi_ObjectReference?
get() = this.obj() as jdi_ObjectReference?
val Value.jdiClass: ClassObjectReference?
get() = this.jdiObj as ClassObjectReference?
fun Value.asJdiValue(vm: VirtualMachine, expectedType: Type): jdi_Value? {
return when (this) {
NULL_VALUE -> null
VOID_VALUE -> vm.mirrorOfVoid()
is IntValue -> when (expectedType) {
Type.BOOLEAN_TYPE -> vm.mirrorOf(boolean)
Type.BYTE_TYPE -> vm.mirrorOf(int.toByte())
Type.SHORT_TYPE -> vm.mirrorOf(int.toShort())
Type.CHAR_TYPE -> vm.mirrorOf(int.toChar())
Type.INT_TYPE -> vm.mirrorOf(int)
else -> throw JDIFailureException("Unknown value type: $this")
}
is LongValue -> vm.mirrorOf(value)
is FloatValue -> vm.mirrorOf(value)
is DoubleValue -> vm.mirrorOf(value)
is ObjectValue -> value as jdi_ObjectReference
is NewObjectValue -> this.obj() as jdi_ObjectReference
else -> throw JDIFailureException("Unknown value: $this")
}
}
@@ -0,0 +1,93 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j
import org.jetbrains.org.objectweb.asm.Opcodes.*
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.FieldInsnNode
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode
open class MemberDescription protected constructor(
val ownerInternalName: String,
val name: String,
val desc: String,
val isStatic: Boolean
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return (other is MemberDescription
&& ownerInternalName == other.ownerInternalName
&& name == other.name
&& desc == other.desc
&& isStatic == other.isStatic
)
}
override fun hashCode(): Int {
var result = 13
result = result * 23 + ownerInternalName.hashCode()
result = result * 23 + name.hashCode()
result = result * 23 + desc.hashCode()
result = result * 23 + isStatic.hashCode()
return result
}
override fun toString() = "MemberDescription(ownerInternalName = $ownerInternalName, name = $name, desc = $desc, isStatic = $isStatic)"
}
val MemberDescription.ownerType: Type
get() = Type.getObjectType(ownerInternalName)
class MethodDescription(
ownerInternalName: String,
name: String,
desc: String,
isStatic: Boolean
) : MemberDescription(ownerInternalName, name, desc, isStatic)
fun MethodDescription(insn: MethodInsnNode): MethodDescription =
MethodDescription(
insn.owner,
insn.name,
insn.desc,
insn.opcode == INVOKESTATIC
)
val MethodDescription.returnType: Type
get() = Type.getReturnType(desc)
val MethodDescription.parameterTypes: List<Type>
get() = Type.getArgumentTypes(desc).toList()
class FieldDescription(
ownerInternalName: String,
name: String,
desc: String,
isStatic: Boolean
) : MemberDescription(ownerInternalName, name, desc, isStatic)
fun FieldDescription(insn: FieldInsnNode): FieldDescription =
FieldDescription(
insn.owner,
insn.name,
insn.desc,
insn.opcode in setOf(GETSTATIC, PUTSTATIC)
)
val FieldDescription.fieldType: Type
get() = Type.getType(desc)
@@ -0,0 +1,134 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.LabelNode
interface Value : org.jetbrains.org.objectweb.asm.tree.analysis.Value {
val asmType: Type
val valid: Boolean
override fun getSize(): Int = asmType.size
override fun toString(): String
}
object NOT_A_VALUE: Value {
override val asmType = Type.getObjectType("<invalid>")
override val valid = false
override fun getSize(): Int = 1
override fun toString() = "NOT_A_VALUE"
}
object VOID_VALUE: Value {
override val asmType: Type = Type.VOID_TYPE
override val valid: Boolean = false
override fun toString() = "VOID_VALUE"
}
fun makeNotInitializedValue(t: Type): Value? {
return when (t.sort) {
Type.VOID -> null
else -> NotInitialized(t)
}
}
class NotInitialized(override val asmType: Type): Value {
override val valid = false
override fun toString() = "NotInitialized: $asmType"
}
abstract class AbstractValueBase<out V>(
override val asmType: Type
) : Value {
override val valid = true
abstract val value: V
override fun toString() = "$value: $asmType"
override fun equals(other: Any?): Boolean {
if (other !is AbstractValue<*>) return false
return value == other.value && asmType == other.asmType
}
override fun hashCode(): Int {
return value!!.hashCode() + 17 * asmType.hashCode()
}
}
abstract class AbstractValue<V>(
override val value: V,
asmType: Type
) : AbstractValueBase<V>(asmType)
class IntValue(value: Int, asmType: Type): AbstractValue<Int>(value, asmType)
class LongValue(value: Long): AbstractValue<Long>(value, Type.LONG_TYPE)
class FloatValue(value: Float): AbstractValue<Float>(value, Type.FLOAT_TYPE)
class DoubleValue(value: Double): AbstractValue<Double>(value, Type.DOUBLE_TYPE)
open class ObjectValue(value: Any?, asmType: Type): AbstractValue<Any?>(value, asmType)
class NewObjectValue(asmType: Type): ObjectValue(null, asmType) {
override var value: Any? = null
get(): Any? {
return field ?: throw IllegalStateException("Trying to access an unitialized object: $this")
}
}
class LabelValue(value: LabelNode): AbstractValue<LabelNode>(value, Type.VOID_TYPE)
fun boolean(v: Boolean) = IntValue(if (v) 1 else 0, Type.BOOLEAN_TYPE)
fun byte(v: Byte) = IntValue(v.toInt(), Type.BYTE_TYPE)
fun short(v: Short) = IntValue(v.toInt(), Type.SHORT_TYPE)
fun char(v: Char) = IntValue(v.toInt(), Type.CHAR_TYPE)
fun int(v: Int) = IntValue(v, Type.INT_TYPE)
fun long(v: Long) = LongValue(v)
fun float(v: Float) = FloatValue(v)
fun double(v: Double) = DoubleValue(v)
//fun <T> obj(v: T, t: Type = if (v != null) Type.getType(v.javaClass) else Type.getType(Any::class.java)) = ObjectValue(v, t)
val NULL_VALUE = ObjectValue(null, Type.getObjectType("null"))
val Value.boolean: Boolean get() = (this as IntValue).value == 1
val Value.int: Int get() = (this as IntValue).value
val Value.long: Long get() = (this as LongValue).value
val Value.float: Float get() = (this as FloatValue).value
val Value.double: Double get() = (this as DoubleValue).value
fun Value.obj(expectedType: Type = asmType): Any? {
return when {
expectedType == Type.BOOLEAN_TYPE -> this.boolean
expectedType == Type.SHORT_TYPE -> (this as IntValue).int.toShort()
expectedType == Type.BYTE_TYPE -> (this as IntValue).int.toByte()
expectedType == Type.CHAR_TYPE -> (this as IntValue).int.toChar()
else -> (this as AbstractValue<*>).value
}
}
fun <T : Any> T?.checkNull(): T {
if (this == null) {
throwInterpretingException(NullPointerException())
}
return this
}
fun throwInterpretingException(e: Throwable): Nothing {
throw Eval4JInterpretingException(e)
}
fun throwBrokenCodeException(e: Throwable): Nothing {
throw BrokenCode(e)
}
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.jdi.test;
public class Debugee {
public static void main(String[] args) {
// BREAKPOINT
Runtime.getRuntime();
System.out.println("hi");
}
}
@@ -0,0 +1,164 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.jdi.test
import com.sun.jdi.*
import com.sun.jdi.event.BreakpointEvent
import com.sun.jdi.event.ClassPrepareEvent
import junit.framework.TestCase
import junit.framework.TestSuite
import org.jetbrains.eval4j.*
import org.jetbrains.eval4j.jdi.JDIEval
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.eval4j.jdi.jdiObj
import org.jetbrains.eval4j.jdi.makeInitialFrame
import org.jetbrains.eval4j.test.buildTestSuite
import org.jetbrains.eval4j.test.getTestName
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
val DEBUGEE_CLASS = Debugee::class.java
fun suite(): TestSuite {
val connectors = Bootstrap.virtualMachineManager().launchingConnectors()
val connector = connectors[0]
println("Using connector $connector")
val connectorArgs = connector.defaultArguments()
val debugeeName = DEBUGEE_CLASS.name
println("Debugee name: $debugeeName")
connectorArgs["main"]!!.setValue(debugeeName)
connectorArgs["options"]!!.setValue("-classpath out/production/eval4j${File.pathSeparator}out/test/eval4j")
connectorArgs["vmexec"]!!.setValue(connectorArgs["home"]!!.value() + File.separator + "bin" + File.separator + connectorArgs["vmexec"]!!.value())
connectorArgs["home"]!!.setValue("")
val vm = connector.launch(connectorArgs)!!
val req = vm.eventRequestManager().createClassPrepareRequest()
req.addClassFilter("*.Debugee")
req.enable()
val latch = CountDownLatch(1)
var classLoader : ClassLoaderReference? = null
var thread : ThreadReference? = null
Thread {
val eventQueue = vm.eventQueue()
mainLoop@ while (true) {
val eventSet = eventQueue.remove()
for (event in eventSet.eventIterator()) {
when (event) {
is ClassPrepareEvent -> {
val _class = event.referenceType()!!
if (_class.name() == debugeeName) {
for (l in _class.allLineLocations()) {
if (l.method().name() == "main") {
classLoader = l.method().declaringType().classLoader()
val breakpointRequest = vm.eventRequestManager().createBreakpointRequest(l)
breakpointRequest.enable()
println("Breakpoint: $breakpointRequest")
vm.resume()
break
}
}
}
}
is BreakpointEvent -> {
println("Suspended at: " + event.location())
thread = event.thread()
latch.countDown()
break@mainLoop
}
else -> {}
}
}
}
}.start()
vm.resume()
latch.await()
var remainingTests = AtomicInteger(0)
val suite = buildTestSuite {
methodNode, ownerClass, expected ->
remainingTests.incrementAndGet()
object : TestCase(getTestName(methodNode.name)) {
override fun runTest() {
val eval = JDIEval(vm, classLoader!!, thread!!, 0)
val args = if ((methodNode.access and Opcodes.ACC_STATIC) == 0) {
// Instance method
val newInstance = eval.newInstance(Type.getType(ownerClass))
val thisValue = eval.invokeMethod(newInstance, MethodDescription(ownerClass.name, "<init>", "()V", false), listOf(), true)
listOf(thisValue)
}
else {
listOf()
}
val value = interpreterLoop(
methodNode,
makeInitialFrame(methodNode, args),
eval
)
fun ObjectReference?.callToString(): String? {
if (this == null) return "null"
return (eval.invokeMethod(
this.asValue(),
MethodDescription(
"java/lang/Object",
"toString",
"()Ljava/lang/String;",
false
),
listOf()).jdiObj as StringReference).value()
}
try {
if (expected is ValueReturned && value is ValueReturned && value.result is ObjectValue) {
assertEquals(expected.result.obj().toString(), value.result.jdiObj.callToString())
}
else if (expected is ExceptionThrown && value is ExceptionThrown) {
val valueObj = value.exception.obj()
val actual = if (valueObj is ObjectReference) valueObj.callToString() else valueObj.toString()
assertEquals(expected.exception.obj().toString(), actual)
}
else {
assertEquals(expected, value)
}
}
finally {
if (remainingTests.decrementAndGet() == 0) vm.resume()
}
}
}
}
return suite
}
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test;
import java.lang.reflect.Array;
public class ArrayHelper {
public static Object newMultiArray(Class<?> elementType, Integer... dimensions) {
int[] dims = new int[dimensions.length];
int i = 0;
for (Integer dimension : dimensions) {
dims[i] = dimension;
i++;
}
return Array.newInstance(elementType, dims);
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test;
class BaseTestData {
String superCall() {
return "Base";
}
private String invokeSpecialPrivateFun(String s) { return "Derived"; }
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test;
import junit.framework.TestSuite;
import org.jetbrains.eval4j.jdi.test.JdiTestKt;
public class Eval4jTest extends TestSuite {
@SuppressWarnings({"UnnecessaryFullyQualifiedName", "StaticMethodReferencedViaSubclass"})
public static TestSuite suite() {
TestSuite eval4jSuite = new TestSuite("Eval4j Tests");
eval4jSuite.addTest(JdiTestKt.suite());
eval4jSuite.addTest(MainKt.suite());
return eval4jSuite;
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreInReflectionTests {
}
@@ -0,0 +1,828 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test;
class TestData extends BaseTestData {
static void returnVoid() {
}
static boolean returnBoolean() {
return true;
}
static byte returnByte() {
return 2;
}
static short returnShort() {
return 2;
}
static char returnChar() {
return '2';
}
static int returnInt() {
return 2;
}
static long returnLong() {
return 2;
}
static float returnFloat() {
return 2.0f;
}
static double returnDouble() {
return 2.0d;
}
static Object returnNull() {
return null;
}
static String returnString() {
return "str";
}
static Object returnStringAsObject() {
return "str";
}
static void checkCastNull() {
CheckCastToNull klass = new CheckCastToNull();
klass.f1(null);
klass.f2(null);
klass.f3(null);
Integer integer = (Integer) null;
Object object = (Object) null;
}
static class CheckCastToNull {
void f1(Integer p) {}
void f2(Integer[] p) {}
void f3(Integer[][] p) {}
}
static Integer integerValueOf() { return 1; }
static Byte byteValueOf() { return 1; }
static Short shortValueOf() { return 1; }
static Long longValueOf() { return 1L; }
static Float floatValueOf() { return 1.0f; }
static Double doubleValueOf() { return 1.0; }
static Character charValueOf() { return 1; }
static Boolean booleanValueOf() { return true; }
static void castFieldTypes() {
CastFieldType.i = 1;
CastFieldType.b = 1;
CastFieldType.s = 1;
CastFieldType.c = 1;
CastFieldType.bool = true;
CastFieldType.l = 1;
CastFieldType.f = 1.0f;
CastFieldType.d = 1.0;
CastFieldType klass = new CastFieldType();
klass.im = 1;
klass.bm = 1;
klass.sm = 1;
klass.cm = 1;
klass.boolm = true;
klass.lm = 1;
klass.fm = 1;
klass.dm = 1;
Integer i = klass.im;
Byte b = klass.bm;
Short s = klass.sm;
Character c = klass.cm;
Boolean bool = klass.boolm;
Long l = klass.lm;
Float f = klass.fm;
Double d = klass.dm;
int i2 = klass.bm;
int i3 = klass.sm;
int i4 = klass.cm;
}
static class CastFieldType {
static int i = 1;
static byte b = 1;
static short s = 1;
static char c = 1;
static boolean bool = true;
static long l = 1;
static float f = 1;
static double d = 1;
int im = 1;
byte bm = 1;
short sm = 1;
char cm = 1;
boolean boolm = true;
long lm = 1;
float fm = 1;
double dm = 1;
}
static void castArrayElementType() {
int[] i = new int[1];
i[0] = 1;
short[] s = new short[1];
s[0] = 1;
byte[] b = new byte[1];
b[0] = 1;
char[] c = new char[1];
c[0] = 1;
boolean[] bool = new boolean[1];
bool[0] = true;
long[] l = new long[1];
l[0] = 1;
float[] f = new float[1];
f[0] = 1;
double[] d = new double[1];
d[0] = 1;
}
static int variable() {
int i = 153;
return i;
}
static int unaryMinus() {
int i = 153;
return -i;
}
static int ifThen() {
boolean a = true;
if (a)
return 2;
return 1;
}
static int ifElse() {
boolean a = false;
if (a) {
return 2;
}
else {
return 1;
}
}
static int loop() {
int i = 0;
while (i < 10) i++;
return i;
}
static int loopWithBreak() {
int i = 0;
while (true) {
if (i > 10) break;
i++;
}
return i;
}
static int loopWithReturn() {
int i = 0;
while (true) {
if (i > 10) return i;
i++;
}
}
static int simpleFinally() {
int i = 5;
try {
return i;
}
finally {
i = 3;
}
}
static int simpleFinallyWithReturn() {
int i = 5;
try {
return i;
}
finally {
return 3;
}
}
static int simpleFinallyWithContinueInLoop() {
int i = 5;
while (true) {
try {
if (i % 2 == 0) continue;
if (i > 10) return i;
}
finally {
i++;
}
}
}
static int simpleFinallyWithBreakInLoop() {
int i = 5;
while (true) {
try {
if (i % 2 == 0) break;
}
finally {
i++;
}
}
return i;
}
static Object call() {
return Integer.valueOf(1);
}
static Object callWithObject() {
return String.valueOf("str");
}
static Object getStaticField() {
return C.FOO;
}
static int FIELD = 0;
static int putStaticField() {
FIELD = 5;
int f1 = FIELD;
FIELD = 6;
int f2 = FIELD;
return f2 + f1;
}
static class C {
static String FOO = "FOO";
int y = 15;
C(int y) {
this.y = y;
}
C() {}
int getY() {
return y;
}
static C newC() {
return new C();
}
static void throwException() {
throw new RuntimeException();
}
}
static int getInstanceField() {
return C.newC().y;
}
static int putInstanceField() {
C c = C.newC();
c.y = 5;
int f1 = c.y;
c.y = 6;
int f2 = c.y;
return f1 + f2;
}
static int instanceMethod() {
return C.newC().getY();
}
static int constructorCallNoArgs() {
return new C().y;
}
static int constructorCallWithArgs() {
return new C(10).y;
}
static class MyEx extends RuntimeException {
final int x;
MyEx(int x) {
this.x = x;
}
}
static int tryCatch() {
try {
throw new MyEx(10);
}
catch (MyEx e) {
return e.x;
}
}
static int tryWiderCatch() {
int a = 10;
try {
if (a > 0) {
throw new MyEx(10);
}
} catch (Exception e) {
return ((MyEx) e).x;
}
return 2;
}
static int classCastException() {
Object a = "";
try {
Integer s = (Integer) a;
}
catch (ClassCastException e) {
return 1;
}
return 2;
}
static String classLiteral() {
return String.class.toString();
}
static int callThrowingMethod() {
try {
C.throwException();
}
catch (RuntimeException e) {
return 1;
}
return 0;
}
static int NPE() {
try {
Object x = null;
x.toString();
}
catch (NullPointerException e) {
return 1;
}
return 0;
}
static Class<?> arrayClass() {
return int[].class;
}
static int arrayOfByte() {
byte[] a = new byte[] {1, 2, 3};
int sum = 0;
for (int i : a) {
sum += i;
}
return sum;
}
static int arrayOfShort() {
short[] a = new short[] {1, 2, 3};
int sum = 0;
for (int i : a) {
sum += i;
}
return sum;
}
static int arrayOfChar() {
char[] a = new char[] {1, 2, 3};
int sum = 0;
for (int i : a) {
sum += i;
}
return sum;
}
static int arrayOfInt() {
int[] a = new int[] {1, 2, 3};
int sum = 0;
for (int i : a) {
sum += i;
}
return sum;
}
static int arrayOfLong() {
long[] a = new long[] {1, 2, 3};
int sum = 0;
for (long i : a) {
sum += i;
}
return sum;
}
static float arrayOfFloat() {
float[] a = new float[] {1, 2, 3};
float sum = 0;
for (float i : a) {
sum += i;
}
return sum;
}
static double arrayOfDouble() {
double[] a = new double[] {1, 2, 3};
double sum = 0;
for (double i : a) {
sum += i;
}
return sum;
}
static String arrayOfString() {
String[] a = new String[] {"1", "2", "3"};
String sum = "";
for (String i : a) {
sum += i;
}
return sum;
}
static int arrayOfByte2() {
byte[][] a = new byte[][] {{1}, {2}, {3}};
int sum = 0;
for (byte[] aa: a)
for (int i : aa) {
sum += i;
}
return sum;
}
static int arrayOfShort2() {
short[][] a = new short[][] {{1}, {2}, {3}};
int sum = 0;
for (short[] aa: a)
for (int i : aa) {
sum += i;
}
return sum;
}
static int arrayOfChar2() {
char[][] a = new char[][] {{1}, {2}, {3}};
int sum = 0;
for (char[] aa: a)
for (int i : aa) {
sum += i;
}
return sum;
}
static int arrayOfInt2() {
int[][] a = new int[][] {{1}, {2}, {3}};
int sum = 0;
for (int[] aa: a)
for (int i : aa) {
sum += i;
}
return sum;
}
static int arrayOfLong2() {
long[][] a = new long[][] {{1}, {2}, {3}};
int sum = 0;
for (long[] aa: a)
for (long i : aa) {
sum += i;
}
return sum;
}
static float arrayOfFloat2() {
float[][] a = new float[][] {{1}, {2}, {3}};
float sum = 0;
for (float[] aa: a)
for (float i : aa) {
sum += i;
}
return sum;
}
static double arrayOfDouble2() {
double[][] a = new double[][] {{1}, {2}, {3}};
double sum = 0;
for (double[] aa: a)
for (double i : aa) {
sum += i;
}
return sum;
}
static String arrayOfString2() {
String[][] a = new String[][] {{"1"}, {"2"}, {"3"}};
String sum = "";
for (String[] aa: a)
for (String i : aa) {
sum += i;
}
return sum;
}
static String multiArrayOfInt() {
int[][] a = new int[2][2];
String s = "";
for (int[] x : a)
for (int y : x) {
s += y;
}
return s;
}
static String multiArrayOfString() {
String[][] a = new String[2][2];
for (String[] x : a)
for (int i = 0; i < x.length; i++) {
x[i] = i + "";
}
String s = "";
for (String[] x : a)
for (String y : x) {
s += y;
}
return s;
}
static void castToArray() {
int[] i = (int[]) null;
}
static void classNotLoadedException() {
ClassNotLoadedExceptionTest klass = new ClassNotLoadedExceptionTest();
klass.f1(null);
klass.f2(null);
klass.f3(null);
klass.f4(null, 1);
klass.f5(null, null);
klass.f6(null, null);
ClassNotLoadedExceptionTest.s1(null);
ClassNotLoadedExceptionTest.s2(null);
ClassNotLoadedExceptionTest.s3(null);
ClassNotLoadedExceptionTest.s4(null, 1);
ClassNotLoadedExceptionTest.s5(null, null);
ClassNotLoadedExceptionTest.s6(null, null);
new ClassNotLoadedExceptionTest(null, null, null);
new ClassNotLoadedExceptionTest(null, 1, null, null);
}
static class ClassNotLoadedExceptionTest {
ClassNotLoadedExceptionTest() {}
// instance methods
static class F1 {}
void f1(F1 p) {}
static class F2 {}
void f2(F2[] p) {}
static class F3 {}
void f3(F3[][] p) {}
static class F4 {}
void f4(F4[] p, int p2) {}
static class F5 {}
void f5(F5[] p, int[] p2) {}
static class F6 {}
void f6(F6[] p, int[][] p2) {}
// static methods
static class S1 {}
static void s1(S1 p) {}
static class S2 {}
static void s2(S2[] p) {}
static class S3 {}
static void s3(S3[][] p) {}
static class S4 {}
static void s4(S4[] p, int p2) {}
static class S5 {}
static void s5(S5[] p, int[] p2) {}
static class S6 {}
static void s6(S6[] p, int[][] p2) {}
// constructor
static class C2 {}
static class C1 {}
static class C3 {}
ClassNotLoadedExceptionTest(C1 p, C2[] p2, C3[][] p3) {}
static class C4 {}
ClassNotLoadedExceptionTest(C4 p, int p2, int[] p3, int[][] p4) {}
}
static void loadLibraryClasses() {
LoadLibraryClasses klass = new LoadLibraryClasses();
klass.f1(1);
klass.f2(LoadLibraryClasses.str());
klass.f3(LoadLibraryClasses.c());
klass.f4(LoadLibraryClasses.cl());
}
static class LoadLibraryClasses {
void f1(Integer i) {}
void f2(String s) {}
void f3(Class c) {}
void f4(ClassLoader cl) {}
static ClassLoader cl() { return null; }
static String str() { return null; }
static Class c() { return null; }
}
static int numberIntValue() {
Number n = 1;
return n.intValue();
}
static void getValueFromStack() {
int i = 1;
boolean b = true;
Integer[] IFEQ = new Integer[] { b ? 100 : 200 };
Integer[] IF_ICMPNE = new Integer[] { i == 1 ? 100 : 200 };
long l = 1;
Long[] IFEQ_L = new Long[] { b ? 100L : 200L };
}
@Override
String superCall() {
return "Derived";
}
@IgnoreInReflectionTests
String testInvokeSpecialForSuperCall() {
return super.superCall();
}
@IgnoreInReflectionTests
static String testInvokeSpecial() {
TestData td = new TestData();
return td.invokeSpecialPrivateFun("");
}
private String invokeSpecialPrivateFun(String s) { return "Base"; }
static Throwable exception1() {
throw new IllegalStateException();
}
static void exception2() {
new ExceptionsTest().f1();
}
static void exceptionClassCast() {
ExceptionsTest.Derived test = (ExceptionsTest.Derived) new ExceptionsTest.Base();
}
static class ExceptionsTest {
void f1() {
throw new IllegalStateException();
}
static class Base {}
static class Derived extends Base {}
}
static boolean exceptionIndexOutOfBounds() {
int[] ints = new int[1];
try { int i = ints[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { ints[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
short[] shorts = new short[1];
try { short s = shorts[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { shorts[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
char[] chars = new char[1];
try { char c = chars[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { chars[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
byte[] bytes = new byte[1];
try { byte b = bytes[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { bytes[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
long[] longs = new long[1];
try { long l = longs[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { longs[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
double[] doubles = new double[1];
try { double d = doubles[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { doubles[2] = 1.0; return false; } catch (ArrayIndexOutOfBoundsException e) { }
float[] floats = new float[1];
try { float f = floats[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { floats[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
boolean[] booleans = new boolean[1];
try { boolean bool = booleans[2];return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { booleans[2] = true; return false; } catch (ArrayIndexOutOfBoundsException e) { }
Object[] objects = new Object[1];
try { Object o = objects[2]; return false; } catch (ArrayIndexOutOfBoundsException e) { }
try { objects[2] = 1; return false; } catch (ArrayIndexOutOfBoundsException e) { }
return true;
}
static boolean indexOutOfBoundsForString() {
String str = "";
try { str.charAt(10); return false; } catch (IndexOutOfBoundsException e) { }
try { str.substring(10); return false; } catch (IndexOutOfBoundsException e) { }
return true;
}
static int coerceByte() {
byte[] b = new byte[2];
return b[1];
}
@IgnoreInReflectionTests
public static void invokeMethodWithArrayOfInterfaces() {
BaseToArray[] c = new BaseToArray[1];
TestInvokeWithObjectArray.invokeStaticFun(c);
TestInvokeWithObjectArray.invokeStaticPrivateFun(c);
TestInvokeWithObjectArray myObj = new TestInvokeWithObjectArray();
myObj.invokeMemberFun(c);
myObj.invokeMemberPrivateFun(c);
int i = TestInvokeWithObjectArray.primitiveReturnValue(c) + 1;
}
private static class TestInvokeWithObjectArray {
public static void invokeStaticFun(Object[] o) {
}
public static void invokeStaticPrivateFun(Object[] o) {
}
public void invokeMemberFun(Object[] o) {
}
public void invokeMemberPrivateFun(Object[] o) {
}
public static int primitiveReturnValue(Object[] o) {
return 1;
}
}
private interface BaseToArray {
}
public TestData() {
}
}
@@ -0,0 +1,397 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test
import junit.framework.TestCase
import junit.framework.TestSuite
import org.jetbrains.eval4j.*
import org.jetbrains.org.objectweb.asm.Opcodes.ACC_STATIC
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import java.lang.reflect.*
import java.lang.reflect.Array as JArray
fun suite(): TestSuite = buildTestSuite {
methodNode, ownerClass, expected ->
object : TestCase(getTestName(methodNode.name)) {
override fun runTest() {
if (!isIgnored(methodNode)) {
val value = interpreterLoop(
methodNode,
initFrame(
ownerClass.getInternalName(),
methodNode
),
REFLECTION_EVAL
)
if (expected is ExceptionThrown && value is ExceptionThrown) {
assertEquals(expected.exception.toString(), value.exception.toString())
}
else {
assertEquals(expected.toString(), value.toString())
}
}
}
private fun isIgnored(methodNode: MethodNode): Boolean {
return methodNode.visibleAnnotations?.any {
val annotationDesc = it.desc
annotationDesc != null &&
Type.getType(annotationDesc) == Type.getType(IgnoreInReflectionTests::class.java)
} ?: false
}
}
}
fun Class<*>.getInternalName(): String = Type.getType(this).internalName
fun initFrame(
owner: String,
m: MethodNode
): Frame<Value> {
val current = Frame<Value>(m.maxLocals, m.maxStack)
current.setReturn(makeNotInitializedValue(Type.getReturnType(m.desc)))
var local = 0
if ((m.access and ACC_STATIC) == 0) {
val ctype = Type.getObjectType(owner)
val newInstance = REFLECTION_EVAL.newInstance(ctype)
val thisValue = REFLECTION_EVAL.invokeMethod(newInstance, MethodDescription(owner, "<init>", "()V", false), listOf(), true)
current.setLocal(local++, thisValue)
}
val args = Type.getArgumentTypes(m.desc)
for (i in 0..args.size - 1) {
current.setLocal(local++, makeNotInitializedValue(args[i]))
if (args[i].size == 2) {
current.setLocal(local++, NOT_A_VALUE)
}
}
while (local < m.maxLocals) {
current.setLocal(local++, NOT_A_VALUE)
}
return current
}
fun objectToValue(obj: Any?, expectedType: Type): Value {
return when (expectedType.sort) {
Type.VOID -> VOID_VALUE
Type.BOOLEAN -> boolean(obj as Boolean)
Type.BYTE -> byte(obj as Byte)
Type.SHORT -> short(obj as Short)
Type.CHAR -> char(obj as Char)
Type.INT -> int(obj as Int)
Type.LONG -> long(obj as Long)
Type.DOUBLE -> double(obj as Double)
Type.FLOAT -> float(obj as Float)
Type.OBJECT -> if (obj == null) NULL_VALUE else ObjectValue(obj, expectedType)
else -> throw UnsupportedOperationException("Unsupported result type: $expectedType")
}
}
object REFLECTION_EVAL : Eval {
val lookup = ReflectionLookup(ReflectionLookup::class.java.classLoader!!)
override fun loadClass(classType: Type): Value {
return ObjectValue(findClass(classType), Type.getType(Class::class.java))
}
override fun loadString(str: String): Value = ObjectValue(str, Type.getType(String::class.java))
override fun newInstance(classType: Type): Value {
return NewObjectValue(classType)
}
override fun isInstanceOf(value: Value, targetType: Type): Boolean {
val _class = findClass(targetType)
return _class.isInstance(value.obj())
}
@Suppress("UNCHECKED_CAST")
override fun newArray(arrayType: Type, size: Int): Value {
return ObjectValue(JArray.newInstance(findClass(arrayType).componentType as Class<Any>, size), arrayType)
}
override fun newMultiDimensionalArray(arrayType: Type, dimensionSizes: List<Int>): Value {
return ObjectValue(ArrayHelper.newMultiArray(findClass(arrayType.elementType), *dimensionSizes.toTypedArray()), arrayType)
}
override fun getArrayLength(array: Value): Value {
return int(JArray.getLength(array.obj().checkNull()))
}
override fun getArrayElement(array: Value, index: Value): Value {
val asmType = array.asmType
val elementType = if (asmType.dimensions == 1) asmType.elementType else Type.getType(asmType.descriptor.substring(1))
val arr = array.obj().checkNull()
val ind = index.int
return mayThrow {
when (elementType.sort) {
Type.BOOLEAN -> boolean(JArray.getBoolean(arr, ind))
Type.BYTE -> byte(JArray.getByte(arr, ind))
Type.SHORT -> short(JArray.getShort(arr, ind))
Type.CHAR -> char(JArray.getChar(arr, ind))
Type.INT -> int(JArray.getInt(arr, ind))
Type.LONG -> long(JArray.getLong(arr, ind))
Type.FLOAT -> float(JArray.getFloat(arr, ind))
Type.DOUBLE -> double(JArray.getDouble(arr, ind))
Type.OBJECT,
Type.ARRAY -> {
val value = JArray.get(arr, ind)
if (value == null) NULL_VALUE else ObjectValue(value, Type.getType(value::class.java))
}
else -> throw UnsupportedOperationException("Unsupported array element type: $elementType")
}
}
}
override fun setArrayElement(array: Value, index: Value, newValue: Value) {
val arr = array.obj().checkNull()
val ind = index.int
if (array.asmType.dimensions > 1) {
JArray.set(arr, ind, newValue.obj())
return
}
val elementType = array.asmType.elementType
mayThrow {
when (elementType.sort) {
Type.BOOLEAN -> JArray.setBoolean(arr, ind, newValue.boolean)
Type.BYTE -> JArray.setByte(arr, ind, newValue.int.toByte())
Type.SHORT -> JArray.setShort(arr, ind, newValue.int.toShort())
Type.CHAR -> JArray.setChar(arr, ind, newValue.int.toChar())
Type.INT -> JArray.setInt(arr, ind, newValue.int)
Type.LONG -> JArray.setLong(arr, ind, newValue.long)
Type.FLOAT -> JArray.setFloat(arr, ind, newValue.float)
Type.DOUBLE -> JArray.setDouble(arr, ind, newValue.double)
Type.OBJECT,
Type.ARRAY -> {
JArray.set(arr, ind, newValue.obj())
}
else -> throw UnsupportedOperationException("Unsupported array element type: $elementType")
}
}
}
fun <T> mayThrow(f: () -> T): T {
try {
try {
return f()
}
catch (ite: InvocationTargetException) {
throw ite.cause ?: ite
}
}
catch (e: Throwable) {
throw ThrownFromEvaluatedCodeException(ObjectValue(e, Type.getType(e::class.java)))
}
}
override fun getStaticField(fieldDesc: FieldDescription): Value {
val field = findStaticField(fieldDesc)
val result = mayThrow {field.get(null)}
return objectToValue(result, fieldDesc.fieldType)
}
override fun setStaticField(fieldDesc: FieldDescription, newValue: Value) {
val field = findStaticField(fieldDesc)
val obj = newValue.obj(fieldDesc.fieldType)
mayThrow {field.set(null, obj)}
}
fun findStaticField(fieldDesc: FieldDescription): Field {
assertTrue(fieldDesc.isStatic)
val field = findClass(fieldDesc).findField(fieldDesc)
assertNotNull("Field not found: $fieldDesc", field)
assertTrue("Field is not static: $field", (field!!.modifiers and Modifier.STATIC) != 0)
return field
}
override fun invokeStaticMethod(methodDesc: MethodDescription, arguments: List<Value>): Value {
assertTrue(methodDesc.isStatic)
val method = findClass(methodDesc).findMethod(methodDesc)
assertNotNull("Method not found: $methodDesc", method)
val args = mapArguments(arguments, methodDesc.parameterTypes).toTypedArray()
val result = mayThrow {method!!.invoke(null, *args)}
return objectToValue(result, methodDesc.returnType)
}
fun findClass(memberDesc: MemberDescription): Class<Any> = findClass(Type.getObjectType(memberDesc.ownerInternalName))
fun findClass(asmType: Type): Class<Any> {
val owner = lookup.findClass(asmType)
assertNotNull("Class not found: ${asmType}", owner)
return owner as Class<Any>
}
override fun getField(instance: Value, fieldDesc: FieldDescription): Value {
val obj = instance.obj().checkNull()
val field = findInstanceField(obj, fieldDesc)
return objectToValue(mayThrow {field.get(obj)}, fieldDesc.fieldType)
}
override fun setField(instance: Value, fieldDesc: FieldDescription, newValue: Value) {
val obj = instance.obj().checkNull()
val field = findInstanceField(obj, fieldDesc)
val newObj = newValue.obj(fieldDesc.fieldType)
mayThrow {field.set(obj, newObj)}
}
fun findInstanceField(obj: Any, fieldDesc: FieldDescription): Field {
val _class = obj::class.java
val field = _class.findField(fieldDesc)
assertNotNull("Field not found: $fieldDesc", field)
return field!!
}
override fun invokeMethod(instance: Value, methodDesc: MethodDescription, arguments: List<Value>, invokespecial: Boolean): Value {
if (invokespecial) {
if (methodDesc.name == "<init>") {
// Constructor call
@Suppress("UNCHECKED_CAST")
val _class = findClass((instance as NewObjectValue).asmType)
val ctor = _class.findConstructor(methodDesc)
assertNotNull("Constructor not found: $methodDesc", ctor)
val args = mapArguments(arguments, methodDesc.parameterTypes).toTypedArray()
val result = mayThrow {ctor!!.newInstance(*args)}
instance.value = result
return objectToValue(result, instance.asmType)
}
else {
// TODO
throw UnsupportedOperationException("invokespecial is not suported in reflection eval")
}
}
val obj = instance.obj().checkNull()
val method = obj::class.java.findMethod(methodDesc)
assertNotNull("Method not found: $methodDesc", method)
val args = mapArguments(arguments, methodDesc.parameterTypes).toTypedArray()
val result = mayThrow {method!!.invoke(obj, *args)}
return objectToValue(result, methodDesc.returnType)
}
private fun mapArguments(arguments: List<Value>, expecetedTypes: List<Type>): List<Any?> {
return arguments.zip(expecetedTypes).map {
val (arg, expectedType) = it
arg.obj(expectedType)
}
}
}
class ReflectionLookup(val classLoader: ClassLoader) {
@Suppress("UNCHECKED_CAST")
fun findClass(asmType: Type): Class<*>? {
return when (asmType.sort) {
Type.BOOLEAN -> java.lang.Boolean.TYPE
Type.BYTE -> java.lang.Byte.TYPE
Type.SHORT -> java.lang.Short.TYPE
Type.CHAR -> java.lang.Character.TYPE
Type.INT -> java.lang.Integer.TYPE
Type.LONG -> java.lang.Long.TYPE
Type.FLOAT -> java.lang.Float.TYPE
Type.DOUBLE -> java.lang.Double.TYPE
Type.OBJECT -> classLoader.loadClass(asmType.internalName.replace('/', '.'))
Type.ARRAY -> Class.forName(asmType.descriptor.replace('/', '.'))
else -> throw UnsupportedOperationException("Unsupported type: $asmType")
}
}
}
@Suppress("UNCHECKED_CAST")
fun Class<out Any>.findMethod(methodDesc: MethodDescription): Method? {
for (declared in declaredMethods) {
if (methodDesc.matches(declared)) return declared
}
val fromSuperClass = (superclass as Class<Any>).findMethod(methodDesc)
if (fromSuperClass != null) return fromSuperClass
for (supertype in interfaces) {
val fromSuper = (supertype as Class<Any>).findMethod(methodDesc)
if (fromSuper != null) return fromSuper
}
return null
}
@Suppress("UNCHECKED_CAST")
fun Class<Any>.findConstructor(methodDesc: MethodDescription): Constructor<Any?>? {
for (declared in declaredConstructors) {
if (methodDesc.matches(declared)) return declared as Constructor<Any?>
}
return null
}
fun MethodDescription.matches(ctor: Constructor<*>): Boolean {
val methodParams = ctor.parameterTypes!!
if (parameterTypes.size != methodParams.size) return false
for ((i, p) in parameterTypes.withIndex()) {
if (!p.matches(methodParams[i])) return false
}
return true
}
fun MethodDescription.matches(method: Method): Boolean {
if (name != method.name) return false
val methodParams = method.parameterTypes!!
if (parameterTypes.size != methodParams.size) return false
for ((i, p) in parameterTypes.withIndex()) {
if (!p.matches(methodParams[i])) return false
}
return returnType.matches(method.returnType!!)
}
@Suppress("UNCHECKED_CAST")
fun Class<out Any>.findField(fieldDesc: FieldDescription): Field? {
for (declared in declaredFields) {
if (fieldDesc.matches(declared)) return declared
}
val superclass = superclass
if (superclass != null) {
val fromSuperClass = (superclass as Class<Any>).findField(fieldDesc)
if (fromSuperClass != null) return fromSuperClass
}
for (supertype in interfaces) {
val fromSuper = (supertype as Class<Any>).findField(fieldDesc)
if (fromSuper != null) return fromSuper
}
return null
}
fun FieldDescription.matches(field: Field): Boolean {
if (name != field.name) return false
return fieldType.matches(field.type!!)
}
fun Type.matches(_class: Class<*>): Boolean = this == Type.getType(_class)
@@ -0,0 +1,95 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test
import junit.framework.TestCase
import junit.framework.TestSuite
import org.jetbrains.eval4j.ExceptionThrown
import org.jetbrains.eval4j.InterpreterResult
import org.jetbrains.eval4j.ObjectValue
import org.jetbrains.eval4j.ValueReturned
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.MethodVisitor
import org.jetbrains.org.objectweb.asm.Opcodes.API_VERSION
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import java.lang.reflect.Modifier
import java.lang.reflect.Array as JArray
fun buildTestSuite(
create: (MethodNode, Class<*>, InterpreterResult?) -> TestCase
): TestSuite {
val suite = TestSuite()
val ownerClass = TestData::class.java
ownerClass.classLoader!!.getResourceAsStream(ownerClass.getInternalName() + ".class")!!.use { inputStream ->
ClassReader(inputStream).accept(object : ClassVisitor(API_VERSION) {
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
return object : MethodNode(API_VERSION, access, name, desc, signature, exceptions) {
override fun visitEnd() {
val testCase = buildTestCase(ownerClass, this, create)
if (testCase != null) {
suite.addTest(testCase)
}
}
}
}
}, 0)
}
return suite
}
private fun buildTestCase(ownerClass: Class<TestData>,
methodNode: MethodNode,
create: (MethodNode, Class<out Any?>, InterpreterResult?) -> TestCase): TestCase? {
var expected: InterpreterResult? = null
for (method in ownerClass.declaredMethods) {
if (method.name == methodNode.name) {
val isStatic = (method.modifiers and Modifier.STATIC) != 0
if (method.parameterTypes!!.size > 0) {
println("Skipping method with parameters: $method")
}
else if (!isStatic && !method.name!!.startsWith("test")) {
println("Skipping instance method (should be started with 'test') : $method")
}
else {
method.isAccessible = true
try {
val result = method.invoke(if (isStatic) null else ownerClass.newInstance())
val returnType = Type.getType(method.returnType!!)
expected = ValueReturned(objectToValue(result, returnType))
}
catch (e: UnsupportedOperationException) {
println("Skipping $method: $e")
}
catch (e: Throwable) {
val cause = e.cause ?: e
expected = ExceptionThrown(objectToValue(cause, Type.getType(cause::class.java)) as ObjectValue, ExceptionThrown.ExceptionKind.FROM_EVALUATOR)
}
}
}
}
if (expected == null) {
println("Method not found: ${methodNode.name}")
return null
}
return create(methodNode, ownerClass, expected)
}
@@ -0,0 +1,19 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.eval4j.test
fun getTestName(methodName: String) = if (methodName.startsWith("test")) methodName else "test${methodName.capitalize()}"
@@ -0,0 +1,24 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
compile(project(":compiler:backend"))
compile(project(":compiler:backend-common"))
compile(project(":compiler:light-classes"))
compile(project(":idea:idea-core"))
compile(project(":idea:ide-common"))
compile(project(":idea:jvm-debugger:jvm-debugger-util"))
compile(files("${System.getProperty("java.home")}/../lib/tools.jar"))
compileOnly(intellijPluginDep("stream-debugger"))
testCompile(project(":kotlin-test:kotlin-test-junit"))
testCompile(commonDep("junit:junit"))
}
sourceSets {
"main" { projectDefault() }
"test" { none() }
}
@@ -0,0 +1,331 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.DebuggerUtils
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.ObjectCollectedException
import com.sun.jdi.ReferenceType
import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousClass
import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousClassOrNull
import org.jetbrains.kotlin.descriptors.ScriptDescriptor
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.debugger.breakpoints.getLambdasAtLineIfAny
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.Companion.getOrComputeClassNames
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames.Companion.Cached
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames.Companion.EMPTY
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames.Companion.NonCached
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.isObjectLiteral
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.org.objectweb.asm.Type
import java.util.*
class DebuggerClassNameProvider(
private val debugProcess: DebugProcess,
val findInlineUseSites: Boolean = true,
val alwaysReturnLambdaParentClass: Boolean = true
) {
companion object {
private val CLASS_ELEMENT_TYPES = arrayOf<Class<out PsiElement>>(
KtFile::class.java,
KtClassOrObject::class.java,
KtProperty::class.java,
KtNamedFunction::class.java,
KtFunctionLiteral::class.java,
KtAnonymousInitializer::class.java
)
internal fun getRelevantElement(element: PsiElement?): PsiElement? {
if (element == null) {
return null
}
for (elementType in CLASS_ELEMENT_TYPES) {
if (elementType.isInstance(element)) {
return element
}
}
// Do not copy the array (*elementTypes) if the element is one we look for
return runReadAction { PsiTreeUtil.getNonStrictParentOfType(element, *CLASS_ELEMENT_TYPES) }
}
}
private val inlineUsagesSearcher = InlineCallableUsagesSearcher(debugProcess)
/**
* Returns classes in which the given line number *is* present.
*/
fun getClassesForPosition(position: SourcePosition): List<ReferenceType> = with(debugProcess) {
val lineNumber = runReadAction { position.line }
return doGetClassesForPosition(position)
.flatMap { className -> virtualMachineProxy.classesByName(className) }
.flatMap { referenceType -> findTargetClasses(referenceType, lineNumber) }
}
/**
* Returns classes names in JDI format (my.app.App$Nested) in which the given line number *may be* present.
*/
fun getOuterClassNamesForPosition(position: SourcePosition): List<String> {
return doGetClassesForPosition(position).toList()
}
private fun doGetClassesForPosition(position: SourcePosition): Set<String> {
val relevantElement = runReadAction {
position.elementAt?.let { getRelevantElement(it) }
}
val result = getOrComputeClassNames(relevantElement) { element ->
getOuterClassNamesForElement(element)
}.toMutableSet()
for (lambda in position.readAction(::getLambdasAtLineIfAny)) {
result += getOrComputeClassNames(lambda) { element ->
getOuterClassNamesForElement(element)
}
}
return result
}
@PublishedApi
@Suppress("NON_TAIL_RECURSIVE_CALL")
internal tailrec fun getOuterClassNamesForElement(element: PsiElement?): ComputedClassNames {
if (element == null) return EMPTY
return when (element) {
is KtScript -> {
getClassType(element)?.let { return Cached(it) }
return EMPTY
}
is KtFile -> {
val fileClassName = runReadAction { JvmFileClassUtil.getFileClassInternalName(element) }.toJdiName()
Cached(fileClassName)
}
is KtClassOrObject -> {
val enclosingElementForLocal = runReadAction { KtPsiUtil.getEnclosingElementForLocalDeclaration(element) }
when {
enclosingElementForLocal != null ->
// A local class
getOuterClassNamesForElement(enclosingElementForLocal)
runReadAction { element.isObjectLiteral() } ->
getOuterClassNamesForElement(element.relevantParentInReadAction)
else ->
// Guaranteed to be non-local class or object
element.readAction { _ ->
if (element is KtClass && runReadAction { element.isInterface() }) {
val name = getNameForNonLocalClass(element)
if (name != null)
Cached(listOf(name, name + JvmAbi.DEFAULT_IMPLS_SUFFIX))
else
EMPTY
} else {
getNameForNonLocalClass(element)?.let { Cached(it) } ?: EMPTY
}
}
}
}
is KtProperty -> {
val nonInlineClasses = if (runReadAction { element.isTopLevel }) {
// Top level property
getOuterClassNamesForElement(element.relevantParentInReadAction)
} else {
val enclosingElementForLocal = runReadAction { KtPsiUtil.getEnclosingElementForLocalDeclaration(element) }
if (enclosingElementForLocal != null) {
// Local class
getOuterClassNamesForElement(enclosingElementForLocal)
} else {
val containingClassOrFile = runReadAction {
PsiTreeUtil.getParentOfType(element, KtFile::class.java, KtClassOrObject::class.java)
}
if (containingClassOrFile is KtObjectDeclaration && containingClassOrFile.isCompanionInReadAction) {
// Properties from the companion object can be placed in the companion object's containing class
(getOuterClassNamesForElement(containingClassOrFile.relevantParentInReadAction) +
getOuterClassNamesForElement(containingClassOrFile)).distinct()
} else if (containingClassOrFile != null) {
getOuterClassNamesForElement(containingClassOrFile)
} else {
getOuterClassNamesForElement(element.relevantParentInReadAction)
}
}
}
if (findInlineUseSites && (
element.isInlineInReadAction ||
runReadAction { element.accessors.any { it.hasModifier(KtTokens.INLINE_KEYWORD) } })
) {
nonInlineClasses + inlineUsagesSearcher.findInlinedCalls(element) { this.getOuterClassNamesForElement(it) }
} else {
return NonCached(nonInlineClasses.classNames)
}
}
is KtNamedFunction -> {
val classNamesOfContainingDeclaration = getOuterClassNamesForElement(element.relevantParentInReadAction)
var nonInlineClasses: ComputedClassNames = classNamesOfContainingDeclaration
if (runReadAction { element.name == null || element.isLocal }) {
val nameOfAnonymousClass = runReadAction { getClassType(element) }
if (nameOfAnonymousClass != null) {
nonInlineClasses += Cached(nameOfAnonymousClass)
}
}
if (!findInlineUseSites || !element.isInlineInReadAction) {
return NonCached(nonInlineClasses.classNames)
}
val inlineCallSiteClasses = inlineUsagesSearcher.findInlinedCalls(element) { this.getOuterClassNamesForElement(it) }
nonInlineClasses + inlineCallSiteClasses
}
is KtAnonymousInitializer -> {
val initializerOwner = runReadAction { element.containingDeclaration }
if (initializerOwner is KtObjectDeclaration && initializerOwner.isCompanionInReadAction) {
return getOuterClassNamesForElement(runReadAction { initializerOwner.containingClassOrObject })
}
getOuterClassNamesForElement(initializerOwner)
}
is KtFunctionLiteral -> {
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(element)
val nonInlinedLambdaClassName = runReadAction {
asmTypeForAnonymousClass(typeMapper.bindingContext, element).internalName.toJdiName()
}
if (!alwaysReturnLambdaParentClass && !InlineUtil.isInlinedArgument(element, typeMapper.bindingContext, true)) {
return Cached(nonInlinedLambdaClassName)
}
Cached(nonInlinedLambdaClassName) + getOuterClassNamesForElement(element.relevantParentInReadAction)
}
else -> getOuterClassNamesForElement(element.relevantParentInReadAction)
}
}
// Should be called in a read action
private fun getClassType(element: KtElement): String? {
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(element)
asmTypeForAnonymousClassOrNull(typeMapper.bindingContext, element)?.let { return it.className }
val descriptor = typeMapper.bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, element]
if (descriptor is ScriptDescriptor) {
return typeMapper.mapClass(descriptor).className
}
if (descriptor != null) {
val containingDeclaration = descriptor.containingDeclaration
if (containingDeclaration is ScriptDescriptor) {
return typeMapper.mapClass(containingDeclaration).className
}
}
return null
}
private fun getNameForNonLocalClass(nonLocalClassOrObject: KtClassOrObject): String? {
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(nonLocalClassOrObject)
val descriptor = typeMapper.bindingContext[BindingContext.CLASS, nonLocalClassOrObject] ?: return null
val type = typeMapper.mapClass(descriptor)
if (type.sort != Type.OBJECT) {
return null
}
return type.className
}
private val KtDeclaration.isInlineInReadAction: Boolean
get() = runReadAction { hasModifier(KtTokens.INLINE_KEYWORD) }
private val KtObjectDeclaration.isCompanionInReadAction: Boolean
get() = runReadAction { isCompanion() }
private val PsiElement.relevantParentInReadAction
get() = runReadAction { getRelevantElement(this.parent) }
}
private fun String.toJdiName() = replace('/', '.')
private fun DebugProcess.findTargetClasses(outerClass: ReferenceType, lineAt: Int): List<ReferenceType> {
val vmProxy = virtualMachineProxy
try {
if (!outerClass.isPrepared) {
return emptyList()
}
} catch (e: ObjectCollectedException) {
return emptyList()
}
val targetClasses = ArrayList<ReferenceType>(1)
try {
for (location in outerClass.allLineLocations()) {
val locationLine = location.lineNumber() - 1
if (locationLine < 0) {
// such locations are not correspond to real lines in code
continue
}
if (lineAt == locationLine) {
val method = location.method()
if (method == null || DebuggerUtils.isSynthetic(method) || method.isBridge) {
// skip synthetic methods
continue
}
targetClasses += outerClass
break
}
}
// The same line number may appear in different classes so we have to scan nested classes as well.
// For example, in the next example line 3 appears in both Foo and Foo$Companion.
/* class Foo {
companion object {
val a = Foo() /* line 3 */
}
} */
val nestedTypes = vmProxy.nestedTypes(outerClass)
for (nested in nestedTypes) {
targetClasses += findTargetClasses(nested, lineAt)
}
} catch (_: AbsentInformationException) {
}
return targetClasses
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
abstract class DelegateSourcePosition(private var delegate: SourcePosition) : SourcePosition() {
override fun getFile(): PsiFile = delegate.file
override fun getElementAt(): PsiElement? = delegate.elementAt
override fun getLine(): Int = delegate.line
override fun getOffset(): Int = delegate.offset
override fun openEditor(requestFocus: Boolean): Editor = delegate.openEditor(requestFocus)
override fun canNavigate() = delegate.canNavigate()
override fun canNavigateToSource() = delegate.canNavigateToSource()
override fun navigate(requestFocus: Boolean) {
delegate.navigate(requestFocus)
}
override fun hashCode(): Int = delegate.hashCode()
override fun equals(other: Any?) = delegate == other
override fun toString() = "DSP($delegate)"
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.resolve.diagnostics.DiagnosticSuppressor
class DiagnosticSuppressorForDebugger : DiagnosticSuppressor {
override fun isSuppressed(diagnostic: Diagnostic): Boolean {
val element = diagnostic.psiElement
val containingFile = element.containingFile
if (containingFile is KtCodeFragment) {
val diagnosticFactory = diagnostic.factory
return diagnosticFactory == Errors.UNSAFE_CALL
}
return false
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.engine.DebugProcess
import com.intellij.openapi.application.ex.ApplicationManagerEx
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.ui.MessageType
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.xdebugger.impl.XDebugSessionImpl
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.idea.debugger.DebuggerClassNameProvider.Companion.getRelevantElement
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ComputedClassNames
import org.jetbrains.kotlin.idea.search.usagesSearch.isImportUsage
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.isInsideOf
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.inline.InlineUtil
class InlineCallableUsagesSearcher(private val myDebugProcess: DebugProcess) {
fun findInlinedCalls(
declaration: KtDeclaration,
bindingContext: BindingContext = KotlinDebuggerCaches.getOrCreateTypeMapper(declaration).bindingContext,
transformer: (PsiElement) -> ComputedClassNames
): ComputedClassNames {
if (!checkIfInline(declaration, bindingContext)) {
return ComputedClassNames.EMPTY
}
else {
val searchResult = hashSetOf<PsiElement>()
val declarationName = runReadAction { declaration.name }
val task = Runnable {
ReferencesSearch.search(declaration, getScopeForInlineDeclarationUsages(declaration)).forEach {
if (!runReadAction { it.isImportUsage() }) {
val usage = (it.element as? KtElement)?.let(::getRelevantElement)
if (usage != null && !runReadAction { declaration.isAncestor(usage) }) {
searchResult.add(usage)
}
}
}
}
var isSuccess = true
val applicationEx = ApplicationManagerEx.getApplicationEx()
if (applicationEx.isDispatchThread) {
isSuccess = ProgressManager.getInstance().runProcessWithProgressSynchronously(
task,
"Compute class names for declaration $declarationName",
true,
myDebugProcess.project)
}
else {
ProgressManager.getInstance().runProcess(task, EmptyProgressIndicator())
}
if (!isSuccess) {
XDebugSessionImpl.NOTIFICATION_GROUP.createNotification(
"Debugger can skip some executions of $declarationName because the computation of class names was interrupted",
MessageType.WARNING
).notify(myDebugProcess.project)
}
val results = searchResult.map { transformer(it) }
return ComputedClassNames(results.flatMap { it.classNames }, shouldBeCached = results.all { it.shouldBeCached })
}
}
private fun checkIfInline(declaration: KtDeclaration, bindingContext: BindingContext): Boolean {
val descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, declaration) ?: return false
return when (descriptor) {
is FunctionDescriptor -> InlineUtil.isInline(descriptor)
is PropertyDescriptor -> InlineUtil.hasInlineAccessors(descriptor)
else -> false
}
}
private fun getScopeForInlineDeclarationUsages(inlineDeclaration: KtDeclaration): GlobalSearchScope {
val virtualFile = runReadAction { inlineDeclaration.containingFile.virtualFile }
return if (virtualFile != null && ProjectRootsUtil.isLibraryFile(myDebugProcess.project, virtualFile)) {
myDebugProcess.searchScope.uniteWith(
KotlinSourceFilterScope.librarySources(GlobalSearchScope.allScope(myDebugProcess.project), myDebugProcess.project))
}
else {
myDebugProcess.searchScope
}
}
}
@@ -0,0 +1,170 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.ide.util.ModuleRendererFactory
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications
import com.intellij.ui.components.JBList
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil.findFilesWithExactPackage
import org.jetbrains.kotlin.psi.KtFile
class KotlinAlternativeSourceNotificationProvider(private val myProject: Project) : EditorNotifications.Provider<EditorNotificationPanel>() {
override fun getKey(): Key<EditorNotificationPanel> {
return KEY
}
override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? {
if (!DebuggerSettings.getInstance().SHOW_ALTERNATIVE_SOURCE) {
return null
}
val session = XDebuggerManager.getInstance(myProject).currentSession
if (session == null) {
FILE_PROCESSED_KEY.set(file, null)
return null
}
val position = session.currentPosition
if (file != position?.file) {
FILE_PROCESSED_KEY.set(file, null)
return null
}
if (DumbService.getInstance(myProject).isDumb) return null
val ktFile = PsiManager.getInstance(myProject).findFile(file) as? KtFile ?: return null
val packageFqName = ktFile.packageFqName
val fileName = ktFile.name
val alternativeKtFiles = findFilesWithExactPackage(packageFqName, GlobalSearchScope.allScope(myProject), myProject).filterTo(HashSet()) {
it.name == fileName
}
FILE_PROCESSED_KEY.set(file, true)
if (alternativeKtFiles.size <= 1) {
return null
}
val currentFirstAlternatives: Collection<KtFile> = listOf(ktFile) + alternativeKtFiles.filter { it != ktFile }
val frame = session.currentStackFrame
val locationDeclName: String? = when (frame) {
is JavaStackFrame -> {
val location = frame.descriptor.location
location?.declaringType()?.name()
}
else -> null
}
return AlternativeSourceNotificationPanel(currentFirstAlternatives, myProject, file, locationDeclName)
}
private class AlternativeSourceNotificationPanel(
alternatives: Collection<KtFile>,
project: Project,
file: VirtualFile,
locationDeclName: String?
) : EditorNotificationPanel() {
private class ComboBoxFileElement(val ktFile: KtFile) {
private val label: String by lazy(LazyThreadSafetyMode.NONE) {
val factory = ModuleRendererFactory.findInstance(ktFile)
val moduleRenderer = factory.moduleRenderer
moduleRenderer.getListCellRendererComponent(ourDummyList, ktFile, 1, false, false)
moduleRenderer.text ?: ""
}
override fun toString(): String = label
companion object {
private val ourDummyList = JBList<KtFile>()
}
}
init {
setText("Alternative source available for file ${file.name}")
val items = alternatives.map { ComboBoxFileElement(it) }
myLinksPanel.add(
ComboBox<ComboBoxFileElement>(items.toTypedArray()).apply {
addActionListener {
val context = DebuggerManagerEx.getInstanceEx(project).context
val session = context.debuggerSession
val ktFile = (selectedItem as ComboBoxFileElement).ktFile
val vFile = ktFile.containingFile.virtualFile
when {
session != null && vFile != null ->
session.process.managerThread.schedule(object : DebuggerCommandImpl() {
override fun action() {
if (!StringUtil.isEmpty(locationDeclName)) {
DebuggerUtilsEx.setAlternativeSourceUrl(locationDeclName, vFile.url, project)
}
DebuggerUIUtil.invokeLater {
FileEditorManager.getInstance(project).closeFile(file)
session.refresh(true)
}
}
})
else -> {
FileEditorManager.getInstance(project).closeFile(file)
ktFile.navigate(true)
}
}
}
})
createActionLabel("Disable") {
DebuggerSettings.getInstance().SHOW_ALTERNATIVE_SOURCE = false
FILE_PROCESSED_KEY.set(file, null)
val fileEditorManager = FileEditorManager.getInstance(project)
val editor = fileEditorManager.getSelectedEditor(file)
if (editor != null) {
fileEditorManager.removeTopComponent(editor, this)
}
}
}
}
companion object {
private val KEY = Key.create<EditorNotificationPanel>("KotlinAlternativeSource")
// FIXME: Share AlternativeSourceNotificationProvider.FILE_PROCESSED_KEY
@Suppress("UNCHECKED_CAST", "DEPRECATION")
private val FILE_PROCESSED_KEY = Key.findKeyByName("AlternativeSourceCheckDone") as Key<Boolean>
}
}
@@ -0,0 +1,190 @@
/*
* Copyright 2010-2018 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.idea.debugger
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.xdebugger.frame.XNamedValue
import com.sun.jdi.*
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceProviderBase {
private companion object {
const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt"
tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? {
if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") {
return type
}
return findBaseContinuationSuperSupertype(type.superclass() ?: return null)
}
}
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
return getAsyncStackTrace(stackFrame.stackFrameProxy, suspendContext)
}
fun getAsyncStackTrace(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
val location = frameProxy.location()
if (!location.isInKotlinSources()) {
return null
}
val method = location.safeMethod() ?: return null
val threadReference = frameProxy.threadProxy().threadReference
if (threadReference == null || !threadReference.isSuspended || !suspendContext.debugProcess.canRunEvaluation) {
return null
}
val evaluationContext = EvaluationContextImpl(suspendContext, frameProxy)
val context = ExecutionContext(evaluationContext, frameProxy)
// DebugMetadataKt not found, probably old kotlin-stdlib version
val debugMetadataKtType = context.findClassSafe(DEBUG_METADATA_KT) ?: return null
val asyncContext = AsyncStackTraceContext(context, method, debugMetadataKtType)
return asyncContext.getAsyncStackTraceForSuspendLambda() ?: asyncContext.getAsyncStackTraceForSuspendFunction()
}
private fun AsyncStackTraceContext.getAsyncStackTraceForSuspendLambda(): List<StackFrameItem>? {
if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") {
return null
}
val thisObject = context.frameProxy.thisObject() ?: return null
val thisType = thisObject.referenceType()
if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) {
return null
}
return collectFrames(thisObject)
}
private fun AsyncStackTraceContext.getAsyncStackTraceForSuspendFunction(): List<StackFrameItem>? {
if ("Lkotlin/coroutines/Continuation;)" !in method.signature()) {
return null
}
val frameProxy = context.frameProxy
val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null
val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null
return collectFrames(continuation)
}
private fun AsyncStackTraceContext.collectFrames(continuation: ObjectReference): List<StackFrameItem>? {
val frames = mutableListOf<StackFrameItem>()
collectFramesRecursively(continuation, frames)
return frames
}
private fun AsyncStackTraceContext.collectFramesRecursively(continuation: ObjectReference, consumer: MutableList<StackFrameItem>) {
val continuationType = continuation.referenceType() as? ClassType ?: return
val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return
val location = getLocation(continuation)
val spilledVariables = getSpilledVariables(continuation) ?: emptyList()
if (location != null) {
consumer += StackFrameItem(location, spilledVariables)
}
val completionField = baseContinuationSupertype.fieldByName("completion") ?: return
val completion = continuation.getValue(completionField) as? ObjectReference ?: return
collectFramesRecursively(completion, consumer)
}
private fun AsyncStackTraceContext.getLocation(continuation: ObjectReference): Location? {
val getStackTraceElementMethod = debugMetadataKtType.methodsByName(
"getStackTraceElement",
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)Ljava/lang/StackTraceElement;"
).firstOrNull() ?: return null
val args = listOf(continuation)
val stackTraceElement = context.invokeMethod(debugMetadataKtType, getStackTraceElementMethod, args) as? ObjectReference
?: return null
val stackTraceElementType = stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name }
?: return null
fun getValue(name: String, desc: String): Value? {
val method = stackTraceElementType.methodsByName(name, desc).single()
return context.invokeMethod(stackTraceElement, method, emptyList())
}
val className = (getValue("getClassName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null
val methodName = (getValue("getMethodName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null
val lineNumber = (getValue("getLineNumber", "()I") as? IntegerValue)?.value()?.takeIf { it >= 0 } ?: return null
val locationClass = context.findClassSafe(className) ?: return null
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
}
private fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName(
"getSpilledVariableFieldMapping",
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;"
).firstOrNull() ?: return null
val args = listOf(continuation)
val rawSpilledVariables = context.invokeMethod(debugMetadataKtType, getSpilledVariableFieldMappingMethod, args) as? ArrayReference
?: return null
val length = rawSpilledVariables.length() / 2
val spilledVariables = ArrayList<XNamedValue>(length)
for (index in 0 until length) {
val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue
val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue
val field = continuation.referenceType().fieldByName(fieldName) ?: continue
val valueDescriptor = object : ValueDescriptorImpl(context.project) {
override fun calcValueName() = variableName
override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field)
override fun getDescriptorEvaluation(context: DebuggerContext?) =
throw EvaluateException("Spilled variable evaluation is not supported")
}
spilledVariables += JavaValue.create(
null,
valueDescriptor,
context.evaluationContext,
context.debugProcess.xdebugProcess!!.nodeManager,
false
)
}
return spilledVariables
}
private fun ExecutionContext.findClassSafe(className: String): ClassType? {
return try {
findClass(className) as? ClassType
} catch (e: Throwable) {
null
}
}
private class AsyncStackTraceContext(
val context: ExecutionContext,
val method: Method,
val debugMetadataKtType: ClassType
)
}
@@ -0,0 +1,15 @@
/*
* 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.idea.debugger
import com.intellij.debugger.engine.AsyncStackTraceProvider
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.memory.utils.StackFrameItem
interface KotlinCoroutinesAsyncStackTraceProviderBase : AsyncStackTraceProvider {
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>?
}
@@ -0,0 +1,14 @@
/*
* 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.idea.debugger
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.memory.utils.StackFrameItem
interface KotlinCoroutinesAsyncStackTraceProviderBase {
fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>?
}
@@ -0,0 +1,56 @@
/*
* Copyright 2000-2018 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.idea.debugger
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SimpleConfigurable
import com.intellij.openapi.util.Getter
import com.intellij.util.xmlb.XmlSerializerUtil
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.settings.DebuggerSettingsCategory
import com.intellij.xdebugger.settings.XDebuggerSettings
import org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingConfigurableUi
@State(name = "KotlinDebuggerSettings", storages = arrayOf(Storage("kotlin_debug.xml")))
class KotlinDebuggerSettings : XDebuggerSettings<KotlinDebuggerSettings>("kotlin_debugger"), Getter<KotlinDebuggerSettings> {
var DEBUG_RENDER_DELEGATED_PROPERTIES: Boolean = true
var DEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES: Boolean = true
var DEBUG_IS_FILTER_FOR_STDLIB_ALREADY_ADDED: Boolean = false
companion object {
fun getInstance(): KotlinDebuggerSettings {
return XDebuggerUtil.getInstance()?.getDebuggerSettings(KotlinDebuggerSettings::class.java)!!
}
}
override fun createConfigurables(category: DebuggerSettingsCategory): Collection<Configurable?> {
return when (category) {
DebuggerSettingsCategory.STEPPING ->
listOf(SimpleConfigurable.create(
"reference.idesettings.debugger.kotlin.stepping",
"Kotlin",
KotlinSteppingConfigurableUi::class.java,
this))
DebuggerSettingsCategory.DATA_VIEWS ->
listOf(SimpleConfigurable.create(
"reference.idesettings.debugger.kotlin.data.view",
"Kotlin",
KotlinDelegatedPropertyRendererConfigurableUi::class.java,
this))
else -> listOf()
}
}
override fun getState() = this
override fun get() = this
override fun loadState(state: KotlinDebuggerSettings) {
XmlSerializerUtil.copyBean<KotlinDebuggerSettings>(state, this)
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.KotlinDelegatedPropertyRendererConfigurableUi">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="99d36" class="javax.swing.JCheckBox" binding="renderDelegatedProperties">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="false"/>
<text resource-bundle="org/jetbrains/kotlin/idea/KotlinBundle" key="debugger.data.view.delegated.properties"/>
</properties>
</component>
<vspacer id="c37da">
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger;
import com.intellij.openapi.options.ConfigurableUi;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class KotlinDelegatedPropertyRendererConfigurableUi implements ConfigurableUi<KotlinDebuggerSettings> {
private JCheckBox renderDelegatedProperties;
private JPanel myPanel;
@Override
public void reset(@NotNull KotlinDebuggerSettings settings) {
boolean flag = settings.getDEBUG_RENDER_DELEGATED_PROPERTIES();
renderDelegatedProperties.setSelected(flag);
}
@Override
public boolean isModified(@NotNull KotlinDebuggerSettings settings) {
return settings.getDEBUG_RENDER_DELEGATED_PROPERTIES() != renderDelegatedProperties.isSelected();
}
@Override
public void apply(@NotNull KotlinDebuggerSettings settings) {
settings.setDEBUG_RENDER_DELEGATED_PROPERTIES(renderDelegatedProperties.isSelected());
}
@NotNull
@Override
public JComponent getComponent() {
return myPanel;
}
}
@@ -0,0 +1,196 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.FrameExtraVariablesProvider
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.evaluation.TextWithImportsImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.text.CharArrayUtil
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineEndOffset
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import java.util.*
import kotlin.math.max
import kotlin.math.min
class KotlinFrameExtraVariablesProvider : FrameExtraVariablesProvider {
override fun isAvailable(sourcePosition: SourcePosition, evalContext: EvaluationContext): Boolean {
if (runReadAction { sourcePosition.line } < 0) return false
return sourcePosition.file.fileType == KotlinFileType.INSTANCE && DebuggerSettings.getInstance().AUTO_VARIABLES_MODE
}
override fun collectVariables(
sourcePosition: SourcePosition, evalContext: EvaluationContext, alreadyCollected: MutableSet<String>): Set<TextWithImports> {
return runReadAction { findAdditionalExpressions(sourcePosition) }
}
}
private fun findAdditionalExpressions(position: SourcePosition): Set<TextWithImports> {
val line = position.line
val file = position.file
val vFile = file.virtualFile
val doc = if (vFile != null) FileDocumentManager.getInstance().getDocument(vFile) else null
if (doc == null || doc.lineCount == 0 || line > (doc.lineCount - 1)) {
return emptySet()
}
val offset = file.getLineStartOffset(line)?.takeIf { it > 0 } ?: return emptySet()
val elem = file.findElementAt(offset) ?: return emptySet()
val containingElement = getContainingElement(elem) ?: elem
val limit = getLineRangeForElement(containingElement, doc)
var startLine = max(limit.startOffset, line)
while (startLine - 1 > limit.startOffset && shouldSkipLine(file, doc, startLine - 1)) {
startLine--
}
var endLine = min(limit.endOffset, line)
while (endLine + 1 < limit.endOffset && shouldSkipLine(file, doc, endLine + 1)) {
endLine++
}
val startOffset = file.getLineStartOffset(startLine) ?: return emptySet()
val endOffset = file.getLineEndOffset(endLine) ?: return emptySet()
if (startOffset >= endOffset) return emptySet()
val lineRange = TextRange(startOffset, endOffset)
if (lineRange.isEmpty) return emptySet()
val expressions = LinkedHashSet<TextWithImports>()
val variablesCollector = VariablesCollector(lineRange, expressions)
containingElement.accept(variablesCollector)
return expressions
}
private fun getContainingElement(element: PsiElement): KtElement? {
val contElement = PsiTreeUtil.getParentOfType(element, KtDeclaration::class.java) ?: PsiTreeUtil.getParentOfType(element, KtElement::class.java)
if (contElement is KtProperty && contElement.isLocal) {
val parent = contElement.parent
return getContainingElement(parent)
}
if (contElement is KtDeclarationWithBody) {
return contElement.bodyExpression
}
return contElement
}
private fun getLineRangeForElement(containingElement: PsiElement, doc: Document): TextRange {
val elemRange = containingElement.textRange
return TextRange(doc.getLineNumber(elemRange.startOffset), doc.getLineNumber(elemRange.endOffset))
}
private fun shouldSkipLine(file: PsiFile, doc: Document, line: Int): Boolean {
val start = CharArrayUtil.shiftForward(doc.charsSequence, doc.getLineStartOffset(line), " \n\t")
val end = doc.getLineEndOffset(line)
if (start >= end) {
return true
}
val elemAtOffset = file.findElementAt(start)
val topmostElementAtOffset = CodeInsightUtils.getTopmostElementAtOffset(elemAtOffset!!, start)
return topmostElementAtOffset !is KtDeclaration
}
private class VariablesCollector(
private val myLineRange: TextRange,
private val myExpressions: MutableSet<TextWithImports>
) : KtTreeVisitorVoid() {
override fun visitKtElement(element: KtElement) {
if (element.isInRange()) {
super.visitKtElement(element)
}
}
override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
if (expression.isInRange()) {
val selector = expression.selectorExpression
if (selector is KtReferenceExpression) {
if (isRefToProperty(selector)) {
myExpressions.add(expression.createText())
return
}
}
}
super.visitQualifiedExpression(expression)
}
private fun isRefToProperty(expression: KtReferenceExpression): Boolean {
// NB: analyze() cannot be called here, because DELEGATED_PROPERTY_RESOLVED_CALL will be always null
// Looks like a bug
@Suppress("DEPRECATION")
val context = expression.analyzeWithAllCompilerChecks().bindingContext
val descriptor = context[BindingContext.REFERENCE_TARGET, expression]
if (descriptor is PropertyDescriptor) {
val getter = descriptor.getter
return (getter == null || context[BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, getter] == null) &&
descriptor.compileTimeInitializer == null
}
return false
}
override fun visitReferenceExpression(expression: KtReferenceExpression) {
if (expression.isInRange()) {
if (isRefToProperty(expression)) {
myExpressions.add(expression.createText())
}
}
super.visitReferenceExpression(expression)
}
private fun KtElement.isInRange(): Boolean = myLineRange.intersects(this.textRange)
private fun KtElement.createText(): TextWithImports = TextWithImportsImpl(CodeFragmentKind.EXPRESSION, this.text)
override fun visitClass(klass: KtClass) {
// Do not show expressions used in local classes
}
override fun visitNamedFunction(function: KtNamedFunction) {
// Do not show expressions used in local functions
}
override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) {
// Do not show expressions used in anonymous objects
}
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
// Do not show expressions used in lambdas
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger;
import com.intellij.xdebugger.breakpoints.XLineBreakpointType;
import com.jetbrains.javascript.debugger.JavaScriptDebugAware;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinLineBreakpointType;
public class KotlinJavaScriptDebugAware extends JavaScriptDebugAware {
@Nullable
@Override
public Class<? extends XLineBreakpointType<?>> getBreakpointTypeClass() {
return KotlinLineBreakpointType.class;
}
}
@@ -0,0 +1,358 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.MultiRequestPositionManager
import com.intellij.debugger.NoDataException
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.PositionManagerEx
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.requests.ClassPrepareRequestor
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.Computable
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.compiled.ClsFileImpl
import com.intellij.psi.search.DelegatingGlobalSearchScope
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.ThreeState
import com.intellij.xdebugger.frame.XStackFrame
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import com.sun.jdi.request.ClassPrepareRequest
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineCount
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.debugger.breakpoints.getLambdasAtLineIfAny
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.decompiler.classFile.KtClsFile
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import com.intellij.debugger.engine.DebuggerUtils as JDebuggerUtils
class KotlinPositionManager(private val myDebugProcess: DebugProcess) : MultiRequestPositionManager, PositionManagerEx() {
private val allKotlinFilesScope =
object : DelegatingGlobalSearchScope(
KotlinSourceFilterScope.projectAndLibrariesSources(GlobalSearchScope.allScope(myDebugProcess.project), myDebugProcess.project)
) {
private val projectIndex = ProjectRootManager.getInstance(myDebugProcess.project).fileIndex
private val scopeComparator =
Comparator.comparing(projectIndex::isInSourceContent)
.thenComparing(projectIndex::isInLibrarySource)
.thenComparing { file1, file2 -> super.compare(file1, file2) }
override fun compare(file1: VirtualFile, file2: VirtualFile): Int {
return scopeComparator.compare(file1, file2)
}
}
private val sourceSearchScopes: List<GlobalSearchScope> = listOf(
myDebugProcess.searchScope,
allKotlinFilesScope
)
override fun getAcceptedFileTypes(): Set<FileType> = KotlinFileTypeFactory.KOTLIN_FILE_TYPES_SET
override fun evaluateCondition(context: EvaluationContext, frame: StackFrameProxyImpl, location: Location, expression: String): ThreeState? {
return ThreeState.UNSURE
}
override fun createStackFrame(frame: StackFrameProxyImpl, debugProcess: DebugProcessImpl, location: Location): XStackFrame? {
if (location.isInKotlinSources()) {
return KotlinStackFrame(frame)
}
return null
}
override fun getSourcePosition(location: Location?): SourcePosition? {
if (location == null) throw NoDataException.INSTANCE
val fileName = location.safeSourceName() ?: throw NoDataException.INSTANCE
val lineNumber = location.safeLineNumber()
if (lineNumber < 0) {
throw NoDataException.INSTANCE
}
if (!DebuggerUtils.isKotlinSourceFile(fileName)) throw NoDataException.INSTANCE
val psiFile = getPsiFileByLocation(location)?.let {
replaceWithAlternativeSource(it, location)
}
if (psiFile == null) {
val isKotlinStrataAvailable = location.declaringType().containsKotlinStrata()
if (isKotlinStrataAvailable) {
try {
val javaSourceFileName = location.sourceName("Java")
val javaClassName = JvmClassName.byInternalName(defaultInternalName(location))
val project = myDebugProcess.project
val defaultPsiFile = DebuggerUtils.findSourceFileForClass(
project, sourceSearchScopes, javaClassName, javaSourceFileName, location)
if (defaultPsiFile != null) {
return SourcePosition.createFromLine(defaultPsiFile, 0)
}
}
catch (e: AbsentInformationException) {
// ignored
}
}
throw NoDataException.INSTANCE
}
if (psiFile !is KtFile) throw NoDataException.INSTANCE
val sourceLineNumber = location.safeSourceLineNumber()
if (sourceLineNumber < 0) {
throw NoDataException.INSTANCE
}
val lambdaOrFunIfInside = getLambdaOrFunIfInside(location, psiFile, sourceLineNumber)
if (lambdaOrFunIfInside != null) {
return SourcePosition.createFromElement(lambdaOrFunIfInside.bodyExpression!!)
}
val elementInDeclaration = getElementForDeclarationLine(location, psiFile, sourceLineNumber)
if (elementInDeclaration != null) {
return SourcePosition.createFromElement(elementInDeclaration)
}
if (sourceLineNumber > psiFile.getLineCount() && myDebugProcess.isDexDebug()) {
val (line, ktFile) = ktLocationInfo(location, true, myDebugProcess.project, false, psiFile)
return SourcePosition.createFromLine(ktFile ?: psiFile, line - 1)
}
val sameLineLocations = location.safeMethod()?.safeAllLineLocations()?.filter {
it.safeLineNumber() == lineNumber && it.safeSourceName() == fileName
}
if (sameLineLocations != null) {
// There're several locations for same source line. If same source position would be created for all of them,
// breakpoints at this line will stop on every location.
// Each location is probably some code in arguments between inlined invocations (otherwise same line locations would
// have been merged into one), but it's impossible to correctly map locations to actual source expressions now.
val locationIndex = sameLineLocations.indexOf(location)
if (locationIndex > 0) {
/*
`finally {}` block code is placed in the class file twice.
Unless the debugger metadata is available, we can't figure out if we are inside `finally {}`, so we have to check it using PSI.
This is conceptually wrong and won't work in some cases, but it's still better than nothing.
*/
val elementAt = psiFile.getLineStartOffset(lineNumber)?.let { psiFile.findElementAt(it) }
val isInsideDuplicatedFinally = elementAt != null && elementAt.getStrictParentOfType<KtFinallySection>() != null
if (!isInsideDuplicatedFinally) {
return KotlinReentrantSourcePosition(SourcePosition.createFromLine(psiFile, sourceLineNumber))
}
}
}
return SourcePosition.createFromLine(psiFile, sourceLineNumber)
}
class KotlinReentrantSourcePosition(delegate: SourcePosition) : DelegateSourcePosition(delegate)
private fun replaceWithAlternativeSource(psiFile: PsiFile, location: Location): PsiFile {
fun findAlternativeSource(): PsiFile? {
val qName = location.declaringType().name()
val alternativeFileUrl = DebuggerUtilsEx.getAlternativeSourceUrl(qName, myDebugProcess.project) ?: return null
val alternativePsiFile = VirtualFileManager.getInstance().findFileByUrl(alternativeFileUrl) ?: return null
return psiFile.manager.findFile(alternativePsiFile)
}
return findAlternativeSource() ?: psiFile
}
// Returns a property or a constructor if debugger stops at class declaration
private fun getElementForDeclarationLine(location: Location, file: KtFile, lineNumber: Int): KtElement? {
val lineStartOffset = file.getLineStartOffset(lineNumber) ?: return null
val elementAt = file.findElementAt(lineStartOffset)
val contextElement = getContextElement(elementAt)
if (contextElement !is KtClass) return null
val methodName = location.method().name()
return when {
JvmAbi.isGetterName(methodName) -> {
val parameterForGetter = contextElement.primaryConstructor?.valueParameters?.firstOrNull {
it.hasValOrVar() && it.name != null && JvmAbi.getterName(it.name!!) == methodName
} ?: return null
parameterForGetter
}
methodName == "<init>" -> contextElement.primaryConstructor
else -> null
}
}
private fun getLambdaOrFunIfInside(location: Location, file: KtFile, lineNumber: Int): KtFunction? {
val currentLocationFqName = location.declaringType().name() ?: return null
val start = CodeInsightUtils.getStartLineOffset(file, lineNumber)
val end = CodeInsightUtils.getEndLineOffset(file, lineNumber)
if (start == null || end == null) return null
val literalsOrFunctions = getLambdasAtLineIfAny(file, lineNumber)
if (literalsOrFunctions.isEmpty()) return null
val elementAt = file.findElementAt(start) ?: return null
val typeMapper = KotlinDebuggerCaches.getOrCreateTypeMapper(elementAt)
val currentLocationClassName = JvmClassName.byFqNameWithoutInnerClasses(FqName(currentLocationFqName))
.internalName.replace('/', '.')
for (literal in literalsOrFunctions) {
if (InlineUtil.isInlinedArgument(literal, typeMapper.bindingContext, true)) {
if (isInsideInlineArgument(literal, location, myDebugProcess as DebugProcessImpl, typeMapper.bindingContext)) {
return literal
}
continue
}
val internalClassNames = DebuggerClassNameProvider(myDebugProcess, alwaysReturnLambdaParentClass = false)
.getOuterClassNamesForElement(literal.firstChild)
.classNames
if (internalClassNames.any { it == currentLocationClassName }) {
return literal
}
}
return null
}
private fun getPsiFileByLocation(location: Location): PsiFile? {
val sourceName = location.safeSourceName() ?: return null
val referenceInternalName = try {
if (location.declaringType().containsKotlinStrata()) {
//replace is required for windows
location.sourcePath().replace('\\', '/')
}
else {
defaultInternalName(location)
}
}
catch (e: AbsentInformationException) {
defaultInternalName(location)
}
val className = JvmClassName.byInternalName(referenceInternalName)
val project = myDebugProcess.project
return DebuggerUtils.findSourceFileForClass(project, sourceSearchScopes, className, sourceName, location)
}
private fun defaultInternalName(location: Location): String {
//no stratum or source path => use default one
val referenceFqName = location.declaringType().name()
// JDI names are of form "package.Class$InnerClass"
return referenceFqName.replace('.', '/')
}
override fun getAllClasses(sourcePosition: SourcePosition): List<ReferenceType> {
val psiFile = sourcePosition.file
if (psiFile is KtFile) {
if (!ProjectRootsUtil.isInProjectOrLibSource(psiFile)) return emptyList()
return DebuggerClassNameProvider(myDebugProcess).getClassesForPosition(sourcePosition)
}
if (psiFile is ClsFileImpl) {
val decompiledPsiFile = psiFile.readAction { it.decompiledPsiFile }
if (decompiledPsiFile is KtClsFile && runReadAction { sourcePosition.line } == -1) {
val className = JvmFileClassUtil.getFileClassInternalName(decompiledPsiFile)
return myDebugProcess.virtualMachineProxy.classesByName(className)
}
}
throw NoDataException.INSTANCE
}
fun originalClassNamesForPosition(position: SourcePosition): List<String> {
return DebuggerClassNameProvider(myDebugProcess, findInlineUseSites = false).getOuterClassNamesForPosition(position)
}
override fun locationsOfLine(type: ReferenceType, position: SourcePosition): List<Location> {
if (position.file !is KtFile) {
throw NoDataException.INSTANCE
}
try {
if (myDebugProcess.isDexDebug()) {
val inlineLocations = runReadAction { getLocationsOfInlinedLine(type, position, myDebugProcess.searchScope) }
if (!inlineLocations.isEmpty()) {
return inlineLocations
}
}
val line = position.line + 1
val locations = type.locationsOfLine(KOTLIN_STRATA_NAME, null, line)
if (locations == null || locations.isEmpty()) {
throw NoDataException.INSTANCE
}
return locations.filter { it.sourceName(KOTLIN_STRATA_NAME) == position.file.name }
}
catch (e: AbsentInformationException) {
throw NoDataException.INSTANCE
}
}
@Deprecated("Since Idea 14.0.3 use createPrepareRequests fun")
override fun createPrepareRequest(classPrepareRequestor: ClassPrepareRequestor, sourcePosition: SourcePosition): ClassPrepareRequest? {
return createPrepareRequests(classPrepareRequestor, sourcePosition).firstOrNull()
}
override fun createPrepareRequests(requestor: ClassPrepareRequestor, position: SourcePosition): List<ClassPrepareRequest> {
if (position.file !is KtFile) {
throw NoDataException.INSTANCE
}
return DumbService.getInstance(myDebugProcess.project).runReadActionInSmartMode(Computable {
val classNames = DebuggerClassNameProvider(myDebugProcess).getOuterClassNamesForPosition(position)
classNames.flatMap { name ->
listOfNotNull(
myDebugProcess.requestsManager.createClassPrepareRequest(requestor, name),
myDebugProcess.requestsManager.createClassPrepareRequest(requestor, "$name$*")
)
}
})
}
}
inline fun <U, V> U.readAction(crossinline f: (U) -> V): V {
return runReadAction { f(this) }
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger;
import com.intellij.debugger.PositionManager;
import com.intellij.debugger.PositionManagerFactory;
import com.intellij.debugger.engine.DebugProcess;
import org.jetbrains.annotations.NotNull;
public class KotlinPositionManagerFactory extends PositionManagerFactory {
@Override
public PositionManager createPositionManager(@NotNull DebugProcess process) {
return new KotlinPositionManager(process);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.SourcePositionHighlighter
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.psi.KtFunctionLiteral
class KotlinSourcePositionHighlighter: SourcePositionHighlighter() {
override fun getHighlightRange(sourcePosition: SourcePosition?): TextRange? {
val lambda = sourcePosition?.elementAt?.parent
if (lambda is KtFunctionLiteral) {
return lambda.textRange
}
return null
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.SourcePositionProvider
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerContextUtil
import com.intellij.debugger.impl.PositionUtil
import com.intellij.debugger.ui.tree.FieldDescriptor
import com.intellij.debugger.ui.tree.LocalVariableDescriptor
import com.intellij.debugger.ui.tree.NodeDescriptor
import com.intellij.openapi.project.Project
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.ClassNotPreparedException
import com.sun.jdi.ReferenceType
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContextUtils
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
import org.jetbrains.kotlin.resolve.source.getPsi
class KotlinSourcePositionProvider : SourcePositionProvider() {
override fun computeSourcePosition(descriptor: NodeDescriptor, project: Project, context: DebuggerContextImpl, nearest: Boolean): SourcePosition? {
if (context.frameProxy == null) return null
if (descriptor is FieldDescriptor) {
return computeSourcePosition(descriptor, project, context, nearest)
}
if (descriptor is LocalVariableDescriptor) {
return computeSourcePosition(descriptor, project, context, nearest)
}
return null
}
private fun computeSourcePosition(
descriptor: LocalVariableDescriptor,
project: Project,
context: DebuggerContextImpl,
nearest: Boolean
): SourcePosition? {
val place = PositionUtil.getContextElement(context) ?: return null
if (place.containingFile !is KtFile) return null
val contextElement = getContextElement(place) ?: return null
val codeFragment = KtPsiFactory(project).createExpressionCodeFragment(descriptor.name, contextElement)
val expression = codeFragment.getContentElement()
if (expression is KtSimpleNameExpression) {
val bindingContext = expression.analyze(BodyResolveMode.PARTIAL)
val declarationDescriptor = BindingContextUtils.extractVariableDescriptorFromReference(bindingContext, expression)
val sourceElement = declarationDescriptor?.source
if (sourceElement is KotlinSourceElement) {
val element = sourceElement.getPsi() ?: return null
if (nearest) {
return DebuggerContextUtil.findNearest(context, element, element.containingFile)
}
return SourcePosition.createFromOffset(element.containingFile, element.textOffset)
}
}
return null
}
private fun computeSourcePosition(descriptor: FieldDescriptor, project: Project, context: DebuggerContextImpl, nearest: Boolean): SourcePosition? {
val fieldName = descriptor.field.name()
if (fieldName == AsmUtil.CAPTURED_THIS_FIELD
|| fieldName == AsmUtil.CAPTURED_RECEIVER_FIELD
|| fieldName.startsWith(AsmUtil.LABELED_THIS_FIELD)
) {
return null
}
val type = descriptor.field.declaringType()
val myClass = findClassByType(project, type, context)?.navigationElement as? KtClassOrObject ?: return null
val field = myClass.declarations.firstOrNull { fieldName == it.name } ?: return null
if (nearest) {
return DebuggerContextUtil.findNearest(context, field, myClass.containingFile)
}
return SourcePosition.createFromOffset(field.containingFile, field.textOffset)
}
private fun findClassByType(project: Project, type: ReferenceType, context: DebuggerContextImpl): PsiElement? {
val session = context.debuggerSession
val scope = session?.searchScope ?: GlobalSearchScope.allScope(project)
val className = JvmClassName.byInternalName(type.name()).fqNameForClassNameWithoutDollars.asString()
val myClass = JavaPsiFacade.getInstance(project).findClass(className, scope)
if (myClass != null) return myClass
val position = getLastSourcePosition(type, context)
if (position != null) {
val element = position.elementAt
if (element != null) {
return element.getStrictParentOfType<KtClassOrObject>()
}
}
return null
}
private fun getLastSourcePosition(type: ReferenceType, context: DebuggerContextImpl): SourcePosition? {
val debugProcess = context.debugProcess
if (debugProcess != null) {
try {
val locations = type.allLineLocations()
if (!locations.isEmpty()) {
val lastLocation = locations.get(locations.size - 1)
return debugProcess.positionManager.getSourcePosition(lastLocation)
}
}
catch (ignored: AbsentInformationException) {
}
catch (ignored: ClassNotPreparedException) {
}
}
return null
}
}
@@ -0,0 +1,416 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.descriptors.data.DescriptorData
import com.intellij.debugger.impl.descriptors.data.DisplayKey
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.debugger.ui.impl.watch.ThisDescriptorImpl
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiExpression
import com.intellij.util.IncorrectOperationException
import com.intellij.xdebugger.frame.XValue
import com.intellij.xdebugger.frame.XValueChildrenList
import com.sun.jdi.ObjectReference
import com.sun.jdi.ReferenceType
import com.sun.jdi.Type
import com.sun.jdi.Value
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.AsmUtil.THIS
import org.jetbrains.kotlin.codegen.DESTRUCTURED_LAMBDA_ARGUMENT_VARIABLE_PREFIX
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.codegen.inline.isFakeLocalVariableForInline
import org.jetbrains.org.objectweb.asm.Type as AsmType
import org.jetbrains.kotlin.utils.getSafe
import java.lang.reflect.Modifier
import java.util.*
import kotlin.coroutines.Continuation
class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDescriptorImpl(frame, MethodsTracker()), true) {
private companion object {
private val LOG = Logger.getInstance(this::class.java)
}
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
override fun superBuildVariables(evaluationContext: EvaluationContextImpl, children: XValueChildrenList) {
if (!kotlinVariableViewService.kotlinVariableView) {
return super.superBuildVariables(evaluationContext, children)
}
val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager
fun addItem(variable: LocalVariableProxyImpl) {
if (nodeManager == null) {
return
}
val variableDescriptor = nodeManager.getLocalVariableDescriptor(null, variable)
children.add(JavaValue.create(null, variableDescriptor, evaluationContext, nodeManager, false))
}
val (thisReferences, otherVariables) = visibleVariables
.partition { it.name() == THIS || it is ThisLocalVariable }
if (!removeSyntheticThisObject(evaluationContext, children, thisReferences) && thisReferences.isNotEmpty()) {
val thisLabels = thisReferences.asSequence()
.filterIsInstance<ThisLocalVariable>()
.mapNotNullTo(hashSetOf()) { it.label }
remapThisObjectForOuterThis(evaluationContext, children, thisLabels)
}
thisReferences.forEach(::addItem)
otherVariables.forEach(::addItem)
}
private fun removeSyntheticThisObject(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
thisReferences: List<LocalVariableProxyImpl>
): Boolean {
val thisObject = evaluationContext.frameProxy?.thisObject() ?: return false
if (thisObject.type().isSubtype(CONTINUATION_TYPE)) {
ExistingInstanceThis.find(children)?.remove()
return true
}
val thisObjectType = thisObject.type()
if (thisObjectType.isSubtype(Function::class.java.name) && '$' in thisObjectType.signature()) {
val existingThis = ExistingInstanceThis.find(children)
if (existingThis != null) {
existingThis.remove()
val javaValue = existingThis.value as? JavaValue
if (javaValue != null) {
attachCapturedThisFromLambda(evaluationContext, children, javaValue, thisReferences)
}
}
return true
}
return removeCallSiteThisInInlineFunction(evaluationContext, children)
}
private fun removeCallSiteThisInInlineFunction(evaluationContext: EvaluationContextImpl, children: XValueChildrenList): Boolean {
val frameProxy = evaluationContext.frameProxy
val variables = frameProxy?.safeVisibleVariables() ?: return false
val inlineDepth = getInlineDepth(variables)
val declarationSiteThis = variables.firstOrNull { v ->
val name = v.name()
name.endsWith(INLINE_FUN_VAR_SUFFIX) && name.dropInlineSuffix() == AsmUtil.INLINE_DECLARATION_SITE_THIS
}
if (inlineDepth > 0 && declarationSiteThis != null) {
ExistingInstanceThis.find(children)?.remove()
return true
}
return false
}
private fun attachCapturedThisFromLambda(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
javaValue: JavaValue,
thisReferences: List<LocalVariableProxyImpl>
) {
try {
val value = javaValue.descriptor.calcValue(evaluationContext) as? ObjectReference ?: return
val thisField = value.referenceType().fieldByName(AsmUtil.CAPTURED_THIS_FIELD) ?: return
val thisValue = value.getValue(thisField) as? ObjectReference ?: return
val thisType = thisValue.referenceType()
val unsafeLabel = generateThisLabelUnsafe(thisType) ?: return
val label = checkLabel(unsafeLabel)
if (label != null) {
val thisName = getThisName(label)
if (thisReferences.any { it.name() == thisName }) {
// Avoid label duplication
return
}
}
val thisName = when {
thisReferences.isEmpty() -> THIS
label != null -> getThisName(label)
else -> "$THIS (anonymous fun)"
}
val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager ?: return
val thisDescriptor = nodeManager.getDescriptor(this.descriptor, LabeledThisData(thisName, thisValue))
children.add(JavaValue.create(null, thisDescriptor, evaluationContext, nodeManager, false))
} catch (e: EvaluateException) {
// do nothing
}
}
private fun remapThisObjectForOuterThis(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
existingThisLabels: Set<String>
) {
val thisObject = evaluationContext.frameProxy?.thisObject() ?: return
val variable = ExistingInstanceThis.find(children) ?: return
val thisLabel = generateThisLabel(thisObject.referenceType())?.takeIf { it !in existingThisLabels }
if (thisLabel == null) {
variable.remove()
return
}
// add additional checks?
variable.remapName(getThisName(thisLabel))
}
// Very Dirty Work-around.
// Hopefully, there will be an API for that in 2019.1.
private class ExistingInstanceThis(
private val children: XValueChildrenList,
private val index: Int,
val value: XValue,
private val size: Int
) {
companion object {
private const val THIS_NAME = "this"
fun find(children: XValueChildrenList): ExistingInstanceThis? {
val size = children.size()
for (i in 0 until size) {
if (children.getName(i) == THIS_NAME) {
val valueDescriptor = (children.getValue(i) as? JavaValue)?.descriptor
@Suppress("FoldInitializerAndIfToElvis")
if (valueDescriptor !is ThisDescriptorImpl) {
return null
}
return ExistingInstanceThis(children, i, children.getValue(i), size)
}
}
return null
}
}
fun remapName(newName: String) {
val (names, _) = getLists() ?: return
names[index] = newName
}
fun remove() {
val (names, values) = getLists() ?: return
names.removeAt(index)
values.removeAt(index)
}
private fun getLists(): Lists? {
if (children.size() != size) {
throw IllegalStateException("Children list was modified")
}
var namesList: MutableList<Any?>? = null
var valuesList: MutableList<Any?>? = null
for (field in XValueChildrenList::class.java.declaredFields) {
val mods = field.modifiers
if (Modifier.isPrivate(mods) && Modifier.isFinal(mods) && !Modifier.isStatic(mods) && field.type == List::class.java) {
@Suppress("UNCHECKED_CAST")
val list = (field.getSafe(children) as? MutableList<Any?>)?.takeIf { it.size == size } ?: continue
if (list[index] == THIS_NAME) {
namesList = list
} else if (list[index] === value) {
valuesList = list
}
}
if (namesList != null && valuesList != null) {
return Lists(namesList, valuesList)
}
}
LOG.error(
"Can't find name/value lists, existing fields: "
+ Arrays.toString(XValueChildrenList::class.java.declaredFields)
)
return null
}
private data class Lists(val names: MutableList<Any?>, val values: MutableList<Any?>)
}
override fun getVisibleVariables(): List<LocalVariableProxyImpl> {
val allVisibleVariables = super.getStackFrameProxy().safeVisibleVariables()
if (!kotlinVariableViewService.kotlinVariableView) {
return allVisibleVariables.map { variable ->
if (isFakeLocalVariableForInline(variable.name())) variable.wrapSyntheticInlineVariable() else variable
}
}
val inlineDepth = getInlineDepth(allVisibleVariables)
val (thisVariables, otherVariables) = allVisibleVariables.asSequence()
.filter { !isHidden(it, inlineDepth) }
.partition {
it.name() == THIS
|| it.name() == AsmUtil.THIS_IN_DEFAULT_IMPLS
|| it.name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
|| (INLINED_THIS_REGEX.matches(it.name()))
}
val (mainThis, otherThis) = thisVariables
.sortedByDescending { it.variable }
.let { it.firstOrNull() to it.drop(1) }
val remappedMainThis = mainThis?.clone(THIS, null)
val remappedOther = (otherThis + otherVariables).map { it.remapVariableNameIfNeeded() }
return (listOfNotNull(remappedMainThis) + remappedOther).sortedBy { it.variable }
}
private fun isHidden(variable: LocalVariableProxyImpl, inlineDepth: Int): Boolean {
val name = variable.name()
return isFakeLocalVariableForInline(name)
|| name.startsWith(DESTRUCTURED_LAMBDA_ARGUMENT_VARIABLE_PREFIX)
|| name.startsWith(AsmUtil.LOCAL_FUNCTION_VARIABLE_PREFIX)
|| getInlineDepth(variable.name()) != inlineDepth
|| name == CONTINUATION_VARIABLE_NAME
}
private fun LocalVariableProxyImpl.remapVariableNameIfNeeded(): LocalVariableProxyImpl {
val name = this.name().dropInlineSuffix()
@Suppress("ConvertToStringTemplate")
return when {
isLabeledThisReference() -> {
val label = name.drop(AsmUtil.LABELED_THIS_PARAMETER.length)
clone(getThisName(label), label)
}
name == AsmUtil.THIS_IN_DEFAULT_IMPLS -> clone(THIS + " (outer)", null)
name == AsmUtil.RECEIVER_PARAMETER_NAME -> clone(THIS + " (receiver)", null)
INLINED_THIS_REGEX.matches(name) -> {
val label = generateThisLabel(frame.getValue(this)?.type())
if (label != null) {
clone(getThisName(label), label)
} else {
this@remapVariableNameIfNeeded
}
}
name != this.name() -> {
object : LocalVariableProxyImpl(frame, variable) {
override fun name() = name
}
}
else -> this@remapVariableNameIfNeeded
}
}
private fun generateThisLabel(type: Type?): String? {
return checkLabel(generateThisLabelUnsafe(type) ?: return null)
}
private fun generateThisLabelUnsafe(type: Type?): String? {
val referenceType = type as? ReferenceType ?: return null
return referenceType.name().substringAfterLast('.').substringAfterLast('$')
}
private fun checkLabel(label: String): String? {
if (label.isEmpty() || label.all { it.isDigit() }) {
return null
}
return label
}
private fun String.dropInlineSuffix(): String {
val depth = getInlineDepth(this)
if (depth == 0) {
return this
}
return dropLast(depth * INLINE_FUN_VAR_SUFFIX.length)
}
private fun LocalVariableProxyImpl.clone(name: String, label: String?): LocalVariableProxyImpl {
return object : LocalVariableProxyImpl(frame, variable), ThisLocalVariable {
override fun name() = name
override val label = label
}
}
private fun LocalVariableProxyImpl.isLabeledThisReference(): Boolean {
@Suppress("ConvertToStringTemplate")
return name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
}
}
private interface ThisLocalVariable {
val label: String?
}
private fun LocalVariableProxyImpl.wrapSyntheticInlineVariable(): LocalVariableProxyImpl {
val proxyWrapper = object : StackFrameProxyImpl(frame.threadProxy(), frame.stackFrame, frame.indexFromBottom) {
override fun getValue(localVariable: LocalVariableProxyImpl): Value {
return frame.virtualMachine.mirrorOfVoid()
}
}
return LocalVariableProxyImpl(proxyWrapper, variable)
}
private fun getThisName(label: String): String {
return "$THIS (@$label)"
}
private class LabeledThisData(val name: String, val value: ObjectReference) : DescriptorData<ValueDescriptorImpl>() {
override fun createDescriptorImpl(project: Project): ValueDescriptorImpl {
return object : ValueDescriptorImpl(project, value) {
override fun getName() = this@LabeledThisData.name
override fun calcValue(evaluationContext: EvaluationContextImpl?) = value
override fun canSetValue() = false
override fun getDescriptorEvaluation(context: DebuggerContext?): PsiExpression {
// TODO change to labeled this
val elementFactory = JavaPsiFacade.getElementFactory(myProject)
try {
return elementFactory.createExpressionFromText("this", null)
} catch (e: IncorrectOperationException) {
throw EvaluateException(e.message, e)
}
}
}
}
override fun getDisplayKey(): DisplayKey<ValueDescriptorImpl> = SimpleDisplayKey(this)
override fun equals(other: Any?) = other is LabeledThisData && other.name == name
override fun hashCode() = name.hashCode()
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2018 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.idea.debugger
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.components.ServiceManager
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.impl.XDebuggerUtilImpl
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
class ToggleKotlinVariablesState {
companion object {
private const val KOTLIN_VARIABLE_VIEW = "debugger.kotlin.variable.view"
fun getService(): ToggleKotlinVariablesState {
return ServiceManager.getService(ToggleKotlinVariablesState::class.java)
}
}
var kotlinVariableView = PropertiesComponent.getInstance().getBoolean(KOTLIN_VARIABLE_VIEW, true)
set(newValue) {
field = newValue
PropertiesComponent.getInstance().setValue(KOTLIN_VARIABLE_VIEW, newValue)
}
}
class ToggleKotlinVariablesView : ToggleAction() {
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
override fun update(e: AnActionEvent) {
super.update(e)
val session = XDebugSession.DATA_KEY.getData(e.dataContext)
e.presentation.isEnabledAndVisible = session != null && session.isInKotlinFile()
}
private fun XDebugSession.isInKotlinFile(): Boolean {
val fileExtension = currentPosition?.file?.extension ?: return false
return fileExtension in KotlinFileTypeFactory.KOTLIN_EXTENSIONS
}
override fun isSelected(e: AnActionEvent) = kotlinVariableViewService.kotlinVariableView
override fun setSelected(e: AnActionEvent, state: Boolean) {
kotlinVariableViewService.kotlinVariableView = state
XDebuggerUtilImpl.rebuildAllSessionsViews(e.project)
}
}
@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinBreakpointFiltersPanel">
<grid id="27dc6" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="5736" binding="myConditionsPanel" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="7">
<margin top="2" left="2" bottom="5" right="5"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<clientProperties>
<BorderFactoryClass class="java.lang.String" value="com.intellij.ui.IdeBorderFactory$PlainSmallWithoutIndent"/>
</clientProperties>
<border type="etched" title-resource-bundle="messages/DebuggerBundle" title-key="label.breakpoint.properties.panel.group.conditions"/>
<children>
<grid id="8e867" binding="myInstanceFiltersPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="0">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="17e11" class="javax.swing.JCheckBox" binding="myInstanceFiltersCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<margin top="2" left="2" bottom="0" right="2"/>
<text resource-bundle="messages/DebuggerBundle" key="breakpoint.properties.panel.option.instance.filters"/>
</properties>
</component>
<grid id="5231f" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<hspacer id="eeee7">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
</hspacer>
<xy id="28068" binding="myInstanceFiltersFieldPanel" layout-manager="XYLayout" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</xy>
</children>
</grid>
</children>
</grid>
<grid id="25884" binding="myClassFiltersPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="0">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="729d5" class="javax.swing.JCheckBox" binding="myClassFiltersCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<margin top="2" left="2" bottom="0" right="2"/>
<text resource-bundle="messages/DebuggerBundle" key="breakpoint.properties.panel.option.class.filters"/>
</properties>
</component>
<grid id="9bef6" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<xy id="e3d10" binding="myClassFiltersFieldPanel" layout-manager="XYLayout" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</xy>
<hspacer id="ec2a">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
</hspacer>
</children>
</grid>
</children>
</grid>
<grid id="275ca" binding="myPassCountPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="0">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="7" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="27bbc" class="javax.swing.JCheckBox" binding="myPassCountCheckbox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<margin top="2" left="2" bottom="0" right="2"/>
<text resource-bundle="messages/DebuggerBundle" key="breakpoint.properties.panel.option.pass.count"/>
</properties>
</component>
<grid id="71095" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="33fef" class="javax.swing.JTextField" binding="myPassCountField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
<properties>
<enabled value="false"/>
<horizontalAlignment value="10"/>
</properties>
</component>
<hspacer id="28e6f">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="15" height="-1"/>
</grid>
</constraints>
</hspacer>
</children>
</grid>
</children>
</grid>
<vspacer id="d1c29">
<constraints>
<grid row="3" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</children>
</grid>
</form>
@@ -0,0 +1,403 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints;
import com.intellij.debugger.InstanceFilter;
import com.intellij.debugger.ui.breakpoints.EditClassFiltersDialog;
import com.intellij.debugger.ui.breakpoints.EditInstanceFiltersDialog;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiClass;
import com.intellij.ui.FieldPanel;
import com.intellij.ui.MultiLineTooltipUI;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel;
import com.intellij.xdebugger.impl.breakpoints.XBreakpointBase;
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.java.debugger.breakpoints.properties.JavaBreakpointProperties;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class KotlinBreakpointFiltersPanel<T extends KotlinPropertyBreakpointProperties, B extends XBreakpoint<T>> extends XBreakpointCustomPropertiesPanel<B> {
private JPanel myConditionsPanel;
private JPanel myInstanceFiltersPanel;
private JCheckBox myInstanceFiltersCheckBox;
private JPanel myInstanceFiltersFieldPanel;
private JPanel myClassFiltersPanel;
private JCheckBox myClassFiltersCheckBox;
private JPanel myClassFiltersFieldPanel;
private JPanel myPassCountPanel;
private JCheckBox myPassCountCheckbox;
private JTextField myPassCountField;
private final FieldPanel myInstanceFiltersField;
private final FieldPanel myClassFiltersField;
private ClassFilter[] myClassFilters = ClassFilter.EMPTY_ARRAY;
private ClassFilter[] myClassExclusionFilters = ClassFilter.EMPTY_ARRAY;
private InstanceFilter[] myInstanceFilters = InstanceFilter.EMPTY_ARRAY;
protected final Project myProject;
private PsiClass myBreakpointPsiClass;
public KotlinBreakpointFiltersPanel(Project project) {
myProject = project;
myInstanceFiltersField = new FieldPanel(new MyTextField(), "", null,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
reloadInstanceFilters();
EditInstanceFiltersDialog _dialog = new EditInstanceFiltersDialog(myProject);
_dialog.setFilters(myInstanceFilters);
_dialog.show();
if (_dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
myInstanceFilters = _dialog.getFilters();
updateInstanceFilterEditor(true);
}
}
},
null
);
myClassFiltersField = new FieldPanel(new MyTextField(), "", null,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
reloadClassFilters();
com.intellij.ide.util.ClassFilter classFilter = createClassConditionFilter();
EditClassFiltersDialog _dialog = new EditClassFiltersDialog(myProject, classFilter);
_dialog.setFilters(myClassFilters, myClassExclusionFilters);
_dialog.show();
if (_dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
myClassFilters = _dialog.getFilters();
myClassExclusionFilters = _dialog.getExclusionFilters();
updateClassFilterEditor(true);
}
}
},
null
);
ActionListener updateListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateCheckboxes();
}
};
myPassCountCheckbox.addActionListener(updateListener);
myInstanceFiltersCheckBox.addActionListener(updateListener);
myClassFiltersCheckBox.addActionListener(updateListener);
ToolTipManager.sharedInstance().registerComponent(myClassFiltersField.getTextField());
ToolTipManager.sharedInstance().registerComponent(myInstanceFiltersField.getTextField());
insert(myInstanceFiltersFieldPanel, myInstanceFiltersField);
insert(myClassFiltersFieldPanel, myClassFiltersField);
DebuggerUIUtil.focusEditorOnCheck(myPassCountCheckbox, myPassCountField);
DebuggerUIUtil.focusEditorOnCheck(myInstanceFiltersCheckBox, myInstanceFiltersField.getTextField());
DebuggerUIUtil.focusEditorOnCheck(myClassFiltersCheckBox, myClassFiltersField.getTextField());
}
@NotNull
@Override
public JComponent getComponent() {
return myConditionsPanel;
}
@Override
public boolean isVisibleOnPopup(@NotNull B breakpoint) {
JavaBreakpointProperties properties = breakpoint.getProperties();
if (properties != null) {
return properties.isCOUNT_FILTER_ENABLED() || properties.isCLASS_FILTERS_ENABLED() || properties.isINSTANCE_FILTERS_ENABLED();
}
return false;
}
@Override
public void saveTo(@NotNull B breakpoint) {
JavaBreakpointProperties properties = breakpoint.getProperties();
if (properties == null) {
return;
}
boolean changed = false;
try {
String text = myPassCountField.getText().trim();
int filter = !text.isEmpty() ? Integer.parseInt(text) : 0;
if (filter < 0) filter = 0;
changed = properties.setCOUNT_FILTER(filter);
}
catch (Exception ignored) {
}
changed = properties.setCOUNT_FILTER_ENABLED(properties.getCOUNT_FILTER() > 0 && myPassCountCheckbox.isSelected()) || changed;
reloadInstanceFilters();
reloadClassFilters();
updateInstanceFilterEditor(true);
updateClassFilterEditor(true);
changed = properties.setINSTANCE_FILTERS_ENABLED(myInstanceFiltersField.getText().length() > 0 && myInstanceFiltersCheckBox.isSelected()) || changed;
changed = properties.setCLASS_FILTERS_ENABLED(myClassFiltersField.getText().length() > 0 && myClassFiltersCheckBox.isSelected()) || changed;
changed = properties.setClassFilters(myClassFilters) || changed;
changed = properties.setClassExclusionFilters(myClassExclusionFilters) || changed;
changed = properties.setInstanceFilters(myInstanceFilters) || changed;
if (changed) {
((XBreakpointBase)breakpoint).fireBreakpointChanged();
}
}
private static void insert(JPanel panel, JComponent component) {
panel.setLayout(new BorderLayout());
panel.add(component, BorderLayout.CENTER);
}
@Override
public void loadFrom(@NotNull B breakpoint) {
JavaBreakpointProperties properties = breakpoint.getProperties();
if (properties != null) {
if (properties.getCOUNT_FILTER() > 0) {
myPassCountField.setText(Integer.toString(properties.getCOUNT_FILTER()));
}
else {
myPassCountField.setText("");
}
myPassCountCheckbox.setSelected(properties.isCOUNT_FILTER_ENABLED());
myInstanceFiltersCheckBox.setSelected(properties.isINSTANCE_FILTERS_ENABLED());
myInstanceFiltersField.setEnabled(properties.isINSTANCE_FILTERS_ENABLED());
myInstanceFiltersField.getTextField().setEditable(properties.isINSTANCE_FILTERS_ENABLED());
myInstanceFilters = properties.getInstanceFilters();
updateInstanceFilterEditor(true);
myClassFiltersCheckBox.setSelected(properties.isCLASS_FILTERS_ENABLED());
myClassFiltersField.setEnabled(properties.isCLASS_FILTERS_ENABLED());
myClassFiltersField.getTextField().setEditable(properties.isCLASS_FILTERS_ENABLED());
myClassFilters = properties.getClassFilters();
myClassExclusionFilters = properties.getClassExclusionFilters();
updateClassFilterEditor(true);
XSourcePosition position = breakpoint.getSourcePosition();
// TODO: need to calculate psi class
//myBreakpointPsiClass = breakpoint.getPsiClass();
}
updateCheckboxes();
}
private void updateInstanceFilterEditor(boolean updateText) {
List<String> filters = new ArrayList<String>();
for (InstanceFilter instanceFilter : myInstanceFilters) {
if (instanceFilter.isEnabled()) {
filters.add(Long.toString(instanceFilter.getId()));
}
}
if (updateText) {
myInstanceFiltersField.setText(StringUtil.join(filters, " "));
}
String tipText = concatWithEx(filters, " ", (int)Math.sqrt(myInstanceFilters.length) + 1, "\n");
myInstanceFiltersField.getTextField().setToolTipText(tipText);
}
private class MyTextField extends JTextField {
public MyTextField() {
}
@Override
public String getToolTipText(MouseEvent event) {
reloadClassFilters();
updateClassFilterEditor(false);
reloadInstanceFilters();
updateInstanceFilterEditor(false);
String toolTipText = super.getToolTipText(event);
return getToolTipText().length() == 0 ? null : toolTipText;
}
@Override
public JToolTip createToolTip() {
JToolTip toolTip = new JToolTip(){{
setUI(new MultiLineTooltipUI());
}};
toolTip.setComponent(this);
return toolTip;
}
}
private void reloadClassFilters() {
String filtersText = myClassFiltersField.getText();
ArrayList<ClassFilter> classFilters = new ArrayList<ClassFilter>();
ArrayList<ClassFilter> exclusionFilters = new ArrayList<ClassFilter>();
int startFilter = -1;
for(int i = 0; i <= filtersText.length(); i++) {
if(i < filtersText.length() && !Character.isWhitespace(filtersText.charAt(i))){
if(startFilter == -1) {
startFilter = i;
}
}
else {
if(startFilter >=0) {
if(filtersText.charAt(startFilter) == '-') {
exclusionFilters.add(new ClassFilter(filtersText.substring(startFilter + 1, i)));
}
else {
classFilters.add(new ClassFilter(filtersText.substring(startFilter, i)));
}
startFilter = -1;
}
}
}
for (ClassFilter classFilter : myClassFilters) {
if (!classFilter.isEnabled()) {
classFilters.add(classFilter);
}
}
for (ClassFilter classFilter : myClassExclusionFilters) {
if (!classFilter.isEnabled()) {
exclusionFilters.add(classFilter);
}
}
myClassFilters = classFilters .toArray(new ClassFilter[classFilters .size()]);
myClassExclusionFilters = exclusionFilters.toArray(new ClassFilter[exclusionFilters.size()]);
}
private void reloadInstanceFilters() {
String filtersText = myInstanceFiltersField.getText();
ArrayList<InstanceFilter> idxs = new ArrayList<InstanceFilter>();
int startNumber = -1;
for(int i = 0; i <= filtersText.length(); i++) {
if(i < filtersText.length() && Character.isDigit(filtersText.charAt(i))) {
if(startNumber == -1) {
startNumber = i;
}
}
else {
if(startNumber >=0) {
idxs.add(InstanceFilter.create(filtersText.substring(startNumber, i)));
startNumber = -1;
}
}
}
for (InstanceFilter instanceFilter : myInstanceFilters) {
if (!instanceFilter.isEnabled()) {
idxs.add(instanceFilter);
}
}
myInstanceFilters = idxs.toArray(new InstanceFilter[idxs.size()]);
}
private void updateClassFilterEditor(boolean updateText) {
List<String> filters = new ArrayList<String>();
for (ClassFilter classFilter : myClassFilters) {
if (classFilter.isEnabled()) {
filters.add(classFilter.getPattern());
}
}
List<String> excludeFilters = new ArrayList<String>();
for (ClassFilter classFilter : myClassExclusionFilters) {
if (classFilter.isEnabled()) {
excludeFilters.add("-" + classFilter.getPattern());
}
}
if (updateText) {
String editorText = StringUtil.join(filters, " ");
if(!filters.isEmpty()) {
editorText += " ";
}
editorText += StringUtil.join(excludeFilters, " ");
myClassFiltersField.setText(editorText);
}
int width = (int)Math.sqrt(myClassExclusionFilters.length + myClassFilters.length) + 1;
String tipText = concatWithEx(filters, " ", width, "\n");
if(!filters.isEmpty()) {
tipText += "\n";
}
tipText += concatWithEx(excludeFilters, " ", width, "\n");
myClassFiltersField.getTextField().setToolTipText(tipText);
}
private static String concatWithEx(List<String> s, String concator, int N, String NthConcator) {
String result = "";
int i = 1;
for (Iterator iterator = s.iterator(); iterator.hasNext(); i++) {
String str = (String) iterator.next();
result += str;
if(iterator.hasNext()){
if(i % N == 0){
result += NthConcator;
}
else {
result += concator;
}
}
}
return result;
}
protected com.intellij.ide.util.ClassFilter createClassConditionFilter() {
com.intellij.ide.util.ClassFilter classFilter;
if(myBreakpointPsiClass != null) {
classFilter = new com.intellij.ide.util.ClassFilter() {
@Override
public boolean isAccepted(PsiClass aClass) {
return myBreakpointPsiClass == aClass || aClass.isInheritor(myBreakpointPsiClass, true);
}
};
}
else {
classFilter = null;
}
return classFilter;
}
protected void updateCheckboxes() {
boolean passCountApplicable = true;
if (myInstanceFiltersCheckBox.isSelected() || myClassFiltersCheckBox.isSelected()) {
passCountApplicable = false;
}
myPassCountCheckbox.setEnabled(passCountApplicable);
boolean passCountSelected = myPassCountCheckbox.isSelected();
myInstanceFiltersCheckBox.setEnabled(!passCountSelected);
myClassFiltersCheckBox.setEnabled(!passCountSelected);
myPassCountField.setEditable(myPassCountCheckbox.isSelected());
myPassCountField.setEnabled (myPassCountCheckbox.isSelected());
myInstanceFiltersField.setEnabled(myInstanceFiltersCheckBox.isSelected());
myInstanceFiltersField.getTextField().setEditable(myInstanceFiltersCheckBox.isSelected());
myClassFiltersField.setEnabled(myClassFiltersCheckBox.isSelected());
myClassFiltersField.getTextField().setEditable(myClassFiltersCheckBox.isSelected());
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaBreakpointHandler
import com.intellij.debugger.engine.JavaBreakpointHandlerFactory
class KotlinFieldBreakpointHandlerFactory : JavaBreakpointHandlerFactory {
override fun createHandler(process: DebugProcessImpl): JavaBreakpointHandler? {
return KotlinFieldBreakpointHandler(process)
}
}
class KotlinLineBreakpointHandlerFactory: JavaBreakpointHandlerFactory {
override fun createHandler(process: DebugProcessImpl): JavaBreakpointHandler? {
return KotlinLineBreakpointHandler(process)
}
}
class KotlinFieldBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinFieldBreakpointType::class.java, process)
class KotlinLineBreakpointHandler(process: DebugProcessImpl) : JavaBreakpointHandler(KotlinLineBreakpointType::class.java, process)
@@ -0,0 +1,397 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.PositionUtil
import com.intellij.debugger.requests.Requestor
import com.intellij.debugger.ui.breakpoints.BreakpointCategory
import com.intellij.debugger.ui.breakpoints.BreakpointWithHighlighter
import com.intellij.debugger.ui.breakpoints.FieldBreakpoint
import com.intellij.icons.AllIcons
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.xdebugger.breakpoints.XBreakpoint
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.Method
import com.sun.jdi.ReferenceType
import com.sun.jdi.event.*
import com.sun.jdi.request.EventRequest
import com.sun.jdi.request.MethodEntryRequest
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.resolve.BindingContext
import javax.swing.Icon
class KotlinFieldBreakpoint(
project: Project,
breakpoint: XBreakpoint<KotlinPropertyBreakpointProperties>
): BreakpointWithHighlighter<KotlinPropertyBreakpointProperties>(project, breakpoint) {
companion object {
private val LOG = Logger.getInstance("#org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinFieldBreakpoint")
private val CATEGORY: Key<FieldBreakpoint> = BreakpointCategory.lookup<FieldBreakpoint>("field_breakpoints")
}
private enum class BreakpointType {
FIELD,
METHOD
}
private var breakpointType: BreakpointType = BreakpointType.FIELD
override fun isValid(): Boolean {
if (!BreakpointWithHighlighter.isPositionValid(xBreakpoint.sourcePosition)) return false
return runReadAction {
val field = getField()
field != null && field.isValid
}
}
fun getField(): KtCallableDeclaration? {
val sourcePosition = sourcePosition
return getProperty(sourcePosition)
}
private fun getProperty(sourcePosition: SourcePosition?): KtCallableDeclaration? {
val property: KtProperty? = PositionUtil.getPsiElementAt(project, KtProperty::class.java, sourcePosition)
if (property != null) {
return property
}
val parameter: KtParameter? = PositionUtil.getPsiElementAt(project, KtParameter::class.java, sourcePosition)
if (parameter != null) {
return parameter
}
return null
}
override fun reload() {
super.reload()
val property = getProperty(sourcePosition) ?: return
val propertyName = property.name ?: return
setFieldName(propertyName)
if (property is KtProperty && property.isTopLevel) {
properties.myClassName = JvmFileClassUtil.getFileClassInfoNoResolve(property.getContainingKtFile()).fileClassFqName.asString()
} else {
val ktClass: KtClassOrObject? = PsiTreeUtil.getParentOfType(property, KtClassOrObject::class.java)
if (ktClass is KtClassOrObject) {
val fqName = ktClass.fqName
if (fqName != null) {
properties.myClassName = fqName.asString()
}
}
}
isInstanceFiltersEnabled = false
}
override fun createRequestForPreparedClass(debugProcess: DebugProcessImpl?, refType: ReferenceType?) {
if (debugProcess == null || refType == null) return
val property = getProperty(sourcePosition) ?: return
breakpointType = (computeBreakpointType(property) ?: return)
val vm = debugProcess.virtualMachineProxy
try {
if (properties.WATCH_INITIALIZATION) {
val sourcePosition = sourcePosition
if (sourcePosition != null) {
debugProcess.positionManager
.locationsOfLine(refType, sourcePosition)
.filter { it.method().isConstructor || it.method().isStaticInitializer }
.forEach {
val request = debugProcess.requestsManager.createBreakpointRequest(this, it)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Breakpoint request added")
}
}
}
}
when (breakpointType) {
BreakpointType.FIELD -> {
val field = refType.fieldByName(getFieldName())
if (field != null) {
val manager = debugProcess.requestsManager
if (properties.WATCH_MODIFICATION && vm.canWatchFieldModification()) {
val request = manager.createModificationWatchpointRequest(this, field)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Modification request added")
}
}
if (properties.WATCH_ACCESS && vm.canWatchFieldAccess()) {
val request = manager.createAccessWatchpointRequest(this, field)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Field access request added (field = ${field.name()}; refType = ${refType.name()})")
}
}
}
}
BreakpointType.METHOD -> {
val fieldName = getFieldName()
if (properties.WATCH_ACCESS) {
val getter = refType.methodsByName(JvmAbi.getterName(fieldName)).firstOrNull()
if (getter != null) {
createMethodBreakpoint(debugProcess, refType, getter)
}
}
if (properties.WATCH_MODIFICATION) {
val setter = refType.methodsByName(JvmAbi.setterName(fieldName)).firstOrNull()
if (setter != null) {
createMethodBreakpoint(debugProcess, refType, setter)
}
}
}
}
}
catch (ex: Exception) {
LOG.debug(ex)
}
}
private fun computeBreakpointType(property: KtCallableDeclaration): BreakpointType? {
return runReadAction {
val bindingContext = property.analyze()
var descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, property)
if (descriptor is ValueParameterDescriptor) {
descriptor = bindingContext.get(BindingContext.VALUE_PARAMETER_AS_PROPERTY, descriptor)
}
if (descriptor is PropertyDescriptor) {
if (bindingContext.get(BindingContext.BACKING_FIELD_REQUIRED, descriptor)!!) {
BreakpointType.FIELD
}
else {
BreakpointType.METHOD
}
}
else {
null
}
}
}
private fun createMethodBreakpoint(debugProcess: DebugProcessImpl, refType: ReferenceType, accessor: Method) {
val manager = debugProcess.requestsManager
val line = accessor.safeAllLineLocations().firstOrNull()
if (line != null) {
val request = manager.createBreakpointRequest(this, line)
debugProcess.requestsManager.enableRequest(request)
if (LOG.isDebugEnabled) {
LOG.debug("Breakpoint request added")
}
}
else {
var entryRequest: MethodEntryRequest? = findRequest(debugProcess, MethodEntryRequest::class.java, this)
if (entryRequest == null) {
entryRequest = manager.createMethodEntryRequest(this)!!
if (LOG.isDebugEnabled) {
LOG.debug("Method entry request added (method = ${accessor.name()}; refType = ${refType.name()})")
}
}
else {
entryRequest.disable()
}
entryRequest.addClassFilter(refType)
manager.enableRequest(entryRequest)
}
}
inline private fun <reified T : EventRequest> findRequest(debugProcess: DebugProcessImpl, requestClass: Class<T>, requestor: Requestor): T? {
val requests = debugProcess.requestsManager.findRequests(requestor)
for (eventRequest in requests) {
if (eventRequest::class.java == requestClass) {
return eventRequest as T
}
}
return null
}
override fun evaluateCondition(context: EvaluationContextImpl, event: LocatableEvent): Boolean {
if (breakpointType == BreakpointType.METHOD && !matchesEvent(event)) {
return false
}
return super.evaluateCondition(context, event)
}
fun matchesEvent(event: LocatableEvent): Boolean {
val method = event.location()?.method()
// TODO check property type
return method != null && method.name() in getMethodsName()
}
private fun getMethodsName(): List<String> {
val fieldName = getFieldName()
return listOf(JvmAbi.getterName(fieldName), JvmAbi.setterName(fieldName))
}
override fun getEventMessage(event: LocatableEvent): String {
val location = event.location()!!
val locationQName = location.declaringType().name() + "." + location.method().name()
val locationFileName = try {
location.sourceName()
}
catch (e: AbsentInformationException) {
fileName
}
catch (e: InternalError) {
fileName
}
val locationLine = location.lineNumber()
when (event) {
is ModificationWatchpointEvent-> {
val field = event.field()
return DebuggerBundle.message(
"status.static.field.watchpoint.reached.access",
field.declaringType().name(),
field.name(),
locationQName,
locationFileName,
locationLine)
}
is AccessWatchpointEvent -> {
val field = event.field()
return DebuggerBundle.message(
"status.static.field.watchpoint.reached.access",
field.declaringType().name(),
field.name(),
locationQName,
locationFileName,
locationLine)
}
is MethodEntryEvent -> {
val method = event.method()
return DebuggerBundle.message(
"status.method.entry.breakpoint.reached",
method.declaringType().name() + "." + method.name() + "()",
locationQName,
locationFileName,
locationLine)
}
is MethodExitEvent -> {
val method = event.method()
return DebuggerBundle.message(
"status.method.exit.breakpoint.reached",
method.declaringType().name() + "." + method.name() + "()",
locationQName,
locationFileName,
locationLine)
}
}
return DebuggerBundle.message(
"status.line.breakpoint.reached",
locationQName,
locationFileName,
locationLine)
}
fun setFieldName(fieldName: String) {
properties.myFieldName = fieldName
}
@TestOnly
fun setWatchAccess(value: Boolean) {
properties.WATCH_ACCESS = value
}
@TestOnly
fun setWatchModification(value: Boolean) {
properties.WATCH_MODIFICATION = value
}
@TestOnly
fun setWatchInitialization(value: Boolean) {
properties.WATCH_INITIALIZATION = value
}
override fun getDisabledIcon(isMuted: Boolean): Icon {
val master = DebuggerManagerEx.getInstanceEx(myProject).breakpointManager.findMasterBreakpoint(this)
return when {
isMuted && master == null -> AllIcons.Debugger.Db_muted_disabled_field_breakpoint
isMuted && master != null -> AllIcons.Debugger.Db_muted_dep_field_breakpoint
master != null -> AllIcons.Debugger.Db_dep_field_breakpoint
else -> AllIcons.Debugger.Db_disabled_field_breakpoint
}
}
override fun getSetIcon(isMuted: Boolean): Icon {
return when {
isMuted -> AllIcons.Debugger.Db_muted_field_breakpoint
else -> AllIcons.Debugger.Db_field_breakpoint
}
}
// BUNCH: 182
override fun getInvalidIcon(isMuted: Boolean): Icon {
return AllIcons.Debugger.Db_invalid_breakpoint
}
override fun getVerifiedIcon(isMuted: Boolean): Icon {
return when {
isMuted -> AllIcons.Debugger.Db_muted_field_breakpoint
else -> AllIcons.Debugger.Db_verified_field_breakpoint
}
}
override fun getVerifiedWarningsIcon(isMuted: Boolean): Icon = AllIcons.Debugger.Db_exception_breakpoint
override fun getCategory() = CATEGORY
override fun getDisplayName(): String? {
if (!isValid) {
return DebuggerBundle.message("status.breakpoint.invalid")
}
val className = className
return if (className != null && !className.isEmpty()) className + "." + getFieldName() else getFieldName()
}
private fun getFieldName(): String {
val declaration = getField()
return runReadAction { declaration?.name } ?: "unknown"
}
override fun getEvaluationElement(): PsiElement? {
return getField()
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.DebuggerBundle
import com.intellij.ui.IdeBorderFactory
import com.intellij.util.ui.DialogUtil
import com.intellij.xdebugger.breakpoints.XLineBreakpoint
import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel
import com.intellij.xdebugger.impl.breakpoints.XBreakpointBase
import org.jetbrains.kotlin.idea.KotlinBundle
import java.awt.BorderLayout
import javax.swing.Box
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JPanel
import kotlin.properties.Delegates
class KotlinFieldBreakpointPropertiesPanel: XBreakpointCustomPropertiesPanel<XLineBreakpoint<KotlinPropertyBreakpointProperties>>() {
private var myWatchInitializationCheckBox: JCheckBox by Delegates.notNull()
private var myWatchAccessCheckBox: JCheckBox by Delegates.notNull()
private var myWatchModificationCheckBox: JCheckBox by Delegates.notNull()
override fun getComponent(): JComponent {
myWatchInitializationCheckBox = JCheckBox(KotlinBundle.message("debugger.field.watchpoints.properties.panel.field.initialization.label"))
myWatchAccessCheckBox = JCheckBox(KotlinBundle.message("debugger.field.watchpoints.properties.panel.field.access.label"))
myWatchModificationCheckBox = JCheckBox(KotlinBundle.message("debugger.field.watchpoints.properties.panel.field.modification.label"))
DialogUtil.registerMnemonic(myWatchInitializationCheckBox)
DialogUtil.registerMnemonic(myWatchAccessCheckBox)
DialogUtil.registerMnemonic(myWatchModificationCheckBox)
fun Box.addNewPanelForCheckBox(checkBox: JCheckBox) {
val panel = JPanel(BorderLayout())
panel.add(checkBox, BorderLayout.NORTH)
this.add(panel)
}
val watchBox = Box.createVerticalBox()
watchBox.addNewPanelForCheckBox(myWatchInitializationCheckBox)
watchBox.addNewPanelForCheckBox(myWatchAccessCheckBox)
watchBox.addNewPanelForCheckBox(myWatchModificationCheckBox)
val mainPanel = JPanel(BorderLayout())
val innerPanel = JPanel(BorderLayout())
innerPanel.add(watchBox, BorderLayout.CENTER)
innerPanel.add(Box.createHorizontalStrut(3), BorderLayout.WEST)
innerPanel.add(Box.createHorizontalStrut(3), BorderLayout.EAST)
mainPanel.add(innerPanel, BorderLayout.NORTH)
mainPanel.border = IdeBorderFactory.createTitledBorder(DebuggerBundle.message("label.group.watch.events"), true)
return mainPanel
}
override fun loadFrom(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>) {
myWatchInitializationCheckBox.isSelected = breakpoint.properties.WATCH_INITIALIZATION
myWatchAccessCheckBox.isSelected = breakpoint.properties.WATCH_ACCESS
myWatchModificationCheckBox.isSelected = breakpoint.properties.WATCH_MODIFICATION
}
override fun saveTo(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>) {
var changed = breakpoint.properties.WATCH_ACCESS != myWatchAccessCheckBox.isSelected
breakpoint.properties.WATCH_ACCESS = myWatchAccessCheckBox.isSelected
changed = breakpoint.properties.WATCH_MODIFICATION != myWatchModificationCheckBox.isSelected || changed
breakpoint.properties.WATCH_MODIFICATION = myWatchModificationCheckBox.isSelected
changed = breakpoint.properties.WATCH_INITIALIZATION != myWatchInitializationCheckBox.isSelected || changed
breakpoint.properties.WATCH_INITIALIZATION = myWatchInitializationCheckBox.isSelected
if (changed) {
(breakpoint as XBreakpointBase<*, *, *>).fireBreakpointChanged()
}
}
}
@@ -0,0 +1,178 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.ui.breakpoints.Breakpoint
import com.intellij.debugger.ui.breakpoints.BreakpointManager
import com.intellij.debugger.ui.breakpoints.BreakpointWithHighlighter
import com.intellij.debugger.ui.breakpoints.JavaBreakpointType
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.breakpoints.XBreakpoint
import com.intellij.xdebugger.breakpoints.XLineBreakpoint
import com.intellij.xdebugger.breakpoints.XLineBreakpointType
import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.debugger.breakpoints.dialog.AddFieldBreakpointDialog
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtDeclarationContainer
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtProperty
import javax.swing.JComponent
class KotlinFieldBreakpointType : JavaBreakpointType<KotlinPropertyBreakpointProperties>, XLineBreakpointType<KotlinPropertyBreakpointProperties>(
"kotlin-field", KotlinBundle.message("debugger.field.watchpoints.tab.title")
) {
override fun createJavaBreakpoint(project: Project, breakpoint: XBreakpoint<KotlinPropertyBreakpointProperties>): Breakpoint<KotlinPropertyBreakpointProperties> {
return KotlinFieldBreakpoint(project, breakpoint)
}
override fun canPutAt(file: VirtualFile, line: Int, project: Project): Boolean {
return canPutAt(file, line, project, this::class.java)
}
override fun getPriority() = 120
override fun createBreakpointProperties(file: VirtualFile, line: Int): KotlinPropertyBreakpointProperties? {
return KotlinPropertyBreakpointProperties()
}
override fun addBreakpoint(project: Project, parentComponent: JComponent?): XLineBreakpoint<KotlinPropertyBreakpointProperties>? {
var result: XLineBreakpoint<KotlinPropertyBreakpointProperties>? = null
val dialog = object : AddFieldBreakpointDialog(project) {
override fun validateData(): Boolean {
val className = className
if (className.isEmpty()) {
reportError(project, DebuggerBundle.message("error.field.breakpoint.class.name.not.specified"))
return false
}
val psiClass = JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project))
if (psiClass !is KtLightClass) {
reportError(project, "Couldn't find '$className' class")
return false
}
val fieldName = fieldName
if (fieldName.isEmpty()) {
reportError(project, DebuggerBundle.message("error.field.breakpoint.field.name.not.specified"))
return false
}
result = when (psiClass) {
is KtLightClassForFacade -> {
psiClass.files.asSequence().mapNotNull { createBreakpointIfPropertyExists(it, it, className, fieldName) }.firstOrNull()
}
is KtLightClassForSourceDeclaration -> {
val jetClass = psiClass.kotlinOrigin
createBreakpointIfPropertyExists(jetClass, jetClass.containingKtFile, className, fieldName)
}
else -> null
}
if (result == null) {
reportError(project, DebuggerBundle.message("error.field.breakpoint.field.not.found", className, fieldName, fieldName))
}
return result != null
}
}
dialog.show()
return result
}
private fun createBreakpointIfPropertyExists(
declaration: KtDeclarationContainer,
file: KtFile,
className: String,
fieldName: String
): XLineBreakpoint<KotlinPropertyBreakpointProperties>? {
val project = file.project
val property = declaration.declarations.firstOrNull { it is KtProperty && it.name == fieldName } ?: return null
val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return null
val line = document.getLineNumber(property.textOffset)
return runWriteAction {
XDebuggerManager.getInstance(project).breakpointManager.addLineBreakpoint(
this,
file.virtualFile.url,
line,
KotlinPropertyBreakpointProperties(fieldName, className)
)
}
}
private fun reportError(project: Project, message: String) {
Messages.showMessageDialog(project, message, DebuggerBundle.message("add.field.breakpoint.dialog.title"), Messages.getErrorIcon())
}
override fun isAddBreakpointButtonVisible() = true
override fun getMutedEnabledIcon() = AllIcons.Debugger.Db_muted_field_breakpoint
override fun getDisabledIcon() = AllIcons.Debugger.Db_disabled_field_breakpoint
override fun getEnabledIcon() = AllIcons.Debugger.Db_field_breakpoint
override fun getMutedDisabledIcon() = AllIcons.Debugger.Db_muted_disabled_field_breakpoint
override fun canBeHitInOtherPlaces() = true
override fun getShortText(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>): String? {
val properties = breakpoint.properties
val className = properties.myClassName
return if (!className.isEmpty()) className + "." + properties.myFieldName else properties.myFieldName
}
override fun createProperties(): KotlinPropertyBreakpointProperties? {
return KotlinPropertyBreakpointProperties()
}
override fun createCustomPropertiesPanel(): XBreakpointCustomPropertiesPanel<XLineBreakpoint<KotlinPropertyBreakpointProperties>>? {
return KotlinFieldBreakpointPropertiesPanel()
}
override fun getDisplayText(breakpoint: XLineBreakpoint<KotlinPropertyBreakpointProperties>): String? {
val kotlinBreakpoint = BreakpointManager.getJavaBreakpoint(breakpoint) as? BreakpointWithHighlighter
return if (kotlinBreakpoint != null) {
kotlinBreakpoint.description
}
else {
super.getDisplayText(breakpoint)
}
}
override fun getEditorsProvider() = null
override fun createCustomRightPropertiesPanel(project: Project): XBreakpointCustomPropertiesPanel<XLineBreakpoint<KotlinPropertyBreakpointProperties>>? {
return KotlinBreakpointFiltersPanel(project)
}
override fun isSuspendThreadSupported() = true
}
@@ -0,0 +1,66 @@
/*
* 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.idea.debugger.breakpoints
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.ui.breakpoints.LineBreakpoint
import com.intellij.openapi.project.Project
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.breakpoints.XBreakpoint
import com.intellij.xdebugger.breakpoints.XBreakpointProperties
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.ReferenceType
import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.idea.debugger.isDexDebug
class KotlinLineBreakpoint(
project: Project?,
xBreakpoint: XBreakpoint<out XBreakpointProperties<*>>?
) : LineBreakpoint<JavaLineBreakpointProperties>(project, xBreakpoint) {
override fun processClassPrepare(debugProcess: DebugProcess?, classType: ReferenceType?) {
val sourcePosition = xBreakpoint?.sourcePosition
if (classType != null && sourcePosition != null) {
if (!hasTargetLine(classType, sourcePosition)) {
return
}
}
super.processClassPrepare(debugProcess, classType)
}
/**
* Returns false if `classType` definitely does not contain a location for a given `sourcePosition`.
*/
private fun hasTargetLine(classType: ReferenceType, sourcePosition: XSourcePosition): Boolean {
val allLineLocations = DebuggerUtilsEx.allLineLocations(classType) ?: return true
if (classType.virtualMachine().isDexDebug()) {
return true
}
val fileName = sourcePosition.file.name
val lineNumber = sourcePosition.line + 1
for (location in allLineLocations) {
try {
val kotlinFileName = location.sourceName(KOTLIN_STRATA_NAME)
val kotlinLineNumber = location.lineNumber(KOTLIN_STRATA_NAME)
if (kotlinFileName == fileName && kotlinLineNumber == lineNumber) {
return true
}
} catch (e: AbsentInformationException) {
if (location.sourceName() == fileName && location.lineNumber() == lineNumber) {
return true
}
}
}
return false
}
}
@@ -0,0 +1,152 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints;
import com.intellij.debugger.SourcePosition;
import com.intellij.debugger.ui.breakpoints.Breakpoint;
import com.intellij.debugger.ui.breakpoints.BreakpointManager;
import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType;
import com.intellij.debugger.ui.breakpoints.LineBreakpoint;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.XLineBreakpoint;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.debugger.breakpoints.properties.JavaBreakpointProperties;
import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties;
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager;
import org.jetbrains.kotlin.psi.KtClassInitializer;
import org.jetbrains.kotlin.psi.KtFunction;
import java.util.List;
public class KotlinLineBreakpointType extends JavaLineBreakpointType {
public KotlinLineBreakpointType() {
super("kotlin-line", "Kotlin Line Breakpoints");
}
@NotNull
@Override
public Breakpoint<JavaLineBreakpointProperties> createJavaBreakpoint(
Project project, XBreakpoint<JavaLineBreakpointProperties> breakpoint
) {
return new KotlinLineBreakpoint(project, breakpoint);
}
@Override
public boolean matchesPosition(@NotNull LineBreakpoint<?> breakpoint, @NotNull SourcePosition position) {
JavaBreakpointProperties properties = getProperties(breakpoint);
if (properties == null || properties instanceof JavaLineBreakpointProperties) {
if (position instanceof KotlinPositionManager.KotlinReentrantSourcePosition) {
return false;
}
if (properties != null && ((JavaLineBreakpointProperties) properties).getLambdaOrdinal() == null) return true;
PsiElement containingMethod = getContainingMethod(breakpoint);
if (containingMethod == null) return false;
return inTheMethod(position, containingMethod);
}
return true;
}
@Override
@Nullable
public PsiElement getContainingMethod(@NotNull LineBreakpoint<?> breakpoint) {
SourcePosition position = breakpoint.getSourcePosition();
if (position == null) return null;
JavaBreakpointProperties properties = getProperties(breakpoint);
if (properties instanceof JavaLineBreakpointProperties) {
Integer ordinal = ((JavaLineBreakpointProperties) properties).getLambdaOrdinal();
PsiElement lambda = getLambdaByOrdinal(position, ordinal);
if (lambda != null) return lambda;
}
return getContainingMethod(position.getElementAt());
}
@Nullable
private static JavaBreakpointProperties getProperties(@NotNull LineBreakpoint<?> breakpoint) {
XBreakpoint<?> xBreakpoint = breakpoint.getXBreakpoint();
return xBreakpoint != null ? (JavaBreakpointProperties) xBreakpoint.getProperties() : null;
}
@Nullable
private static KtFunction getLambdaByOrdinal(SourcePosition position, Integer ordinal) {
if (ordinal != null && ordinal >= 0) {
List<KtFunction> lambdas = BreakpointTypeUtilsKt.getLambdasAtLineIfAny(position);
if (lambdas.size() > ordinal) {
return lambdas.get(ordinal);
}
}
return null;
}
@Nullable
public static PsiElement getContainingMethod(@Nullable PsiElement elem) {
//noinspection unchecked
return PsiTreeUtil.getParentOfType(elem, KtFunction.class, KtClassInitializer.class);
}
public static boolean inTheMethod(@NotNull SourcePosition pos, @NotNull PsiElement method) {
PsiElement elem = pos.getElementAt();
if (elem == null) return false;
return Comparing.equal(getContainingMethod(elem), method);
}
@Override
public boolean canPutAt(@NotNull VirtualFile file, int line, @NotNull Project project) {
return BreakpointTypeUtilsKt.canPutAt(file, line, project, getClass());
}
@NotNull
@Override
public List<JavaBreakpointVariant> computeVariants(@NotNull Project project, @NotNull XSourcePosition position) {
return BreakpointTypeUtilsKt.computeVariants(project, position, this);
}
@Nullable
@Override
public TextRange getHighlightRange(XLineBreakpoint<JavaLineBreakpointProperties> breakpoint) {
JavaLineBreakpointProperties properties = breakpoint.getProperties();
if (properties != null) {
Integer ordinal = properties.getLambdaOrdinal();
if (ordinal != null) {
Breakpoint javaBreakpoint = BreakpointManager.getJavaBreakpoint(breakpoint);
if (javaBreakpoint instanceof LineBreakpoint) {
SourcePosition position = ((LineBreakpoint) javaBreakpoint).getSourcePosition();
if (position != null) {
KtFunction lambda = getLambdaByOrdinal(position, ordinal);
if (lambda != null) {
return lambda.getTextRange();
}
}
}
}
}
return null;
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.util.xmlb.annotations.Attribute
import org.jetbrains.java.debugger.breakpoints.properties.JavaBreakpointProperties
class KotlinPropertyBreakpointProperties(
@Attribute var myFieldName: String = "",
@Attribute var myClassName: String = ""
): JavaBreakpointProperties<KotlinPropertyBreakpointProperties>() {
var WATCH_MODIFICATION: Boolean = true
var WATCH_ACCESS: Boolean = false
var WATCH_INITIALIZATION: Boolean = false
override fun getState() = this
override fun loadState(state: KotlinPropertyBreakpointProperties) {
super.loadState(state)
WATCH_MODIFICATION = state.WATCH_MODIFICATION
WATCH_ACCESS = state.WATCH_ACCESS
WATCH_INITIALIZATION = state.WATCH_INITIALIZATION
myFieldName = state.myFieldName
myClassName = state.myClassName
}
}
@@ -0,0 +1,150 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.impl.XSourcePositionImpl
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineNumber
import org.jetbrains.kotlin.idea.debugger.findElementAtLine
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import java.util.*
fun canPutAt(file: VirtualFile, line: Int, project: Project, breakpointTypeClass: Class<*>): Boolean {
val psiFile = PsiManager.getInstance(project).findFile(file)
if (psiFile == null || psiFile.virtualFile?.fileType != KotlinFileType.INSTANCE) {
return false
}
val document = FileDocumentManager.getInstance().getDocument(file) ?: return false
var result: Class<*>? = null
XDebuggerUtil.getInstance().iterateLine(project, document, line, fun (el: PsiElement): Boolean {
// avoid comments
if (el is PsiWhiteSpace || PsiTreeUtil.getParentOfType(el, PsiComment::class.java, false) != null) {
return true
}
var element = el
var parent = element.parent
while (parent != null) {
val offset = parent.textOffset
if (offset >= 0 && document.getLineNumber(offset) != line) break
element = parent
parent = element.parent
}
if (element is KtProperty || element is KtParameter) {
result = if ((element is KtParameter && element.hasValOrVar()) || (element is KtProperty && !element.isLocal)) {
KotlinFieldBreakpointType::class.java
}
else {
KotlinLineBreakpointType::class.java
}
return false
}
else {
result = KotlinLineBreakpointType::class.java
}
return true
})
return result == breakpointTypeClass
}
fun computeVariants(
project: Project,
position: XSourcePosition,
kotlinBreakpointType: KotlinLineBreakpointType
): List<JavaLineBreakpointType.JavaBreakpointVariant> {
val file = PsiManager.getInstance(project).findFile(position.file) as? KtFile ?: return emptyList()
val pos = SourcePosition.createFromLine(file, position.line)
val lambdas = getLambdasAtLineIfAny(pos)
if (lambdas.isEmpty()) return emptyList()
val result = LinkedList<JavaLineBreakpointType.JavaBreakpointVariant>()
val elementAt = pos.elementAt.parentsWithSelf.firstIsInstance<KtElement>()
val mainMethod = KotlinLineBreakpointType.getContainingMethod(elementAt)
if (mainMethod != null) {
result.add(
kotlinBreakpointType.LineJavaBreakpointVariant(
position,
CodeInsightUtils.getTopmostElementAtOffset(elementAt, pos.offset),
-1
)
)
}
lambdas.forEachIndexed { ordinal, lambda ->
val positionImpl = XSourcePositionImpl.createByElement(lambda.bodyExpression)
if (positionImpl != null) {
result.add(kotlinBreakpointType.LambdaJavaBreakpointVariant(positionImpl, lambda, ordinal))
}
}
result.add(kotlinBreakpointType.JavaBreakpointVariant(position))
return result
}
fun getLambdasAtLineIfAny(sourcePosition: SourcePosition): List<KtFunction> {
val file = sourcePosition.file as? KtFile ?: return emptyList()
val lineNumber = sourcePosition.line
return getLambdasAtLineIfAny(file, lineNumber)
}
fun getLambdasAtLineIfAny(file: KtFile, line: Int): List<KtFunction> {
val lineElement = findElementAtLine(file, line) as? KtElement ?: return emptyList()
val start = lineElement.startOffset
val end = lineElement.endOffset
val allLiterals = CodeInsightUtils.
findElementsOfClassInRange(file, start, end, KtFunction::class.java)
.filterIsInstance<KtFunction>()
// filter function literals and functional expressions
.filter { it is KtFunctionLiteral || it.name == null }
.toSet()
return allLiterals.filter {
val statement = it.bodyBlockExpression?.statements?.firstOrNull() ?: it
statement.getLineNumber() == line && statement.getLineNumber(false) == line
}
}
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.breakpoints.dialog.AddFieldBreakpointDialog">
<grid id="dbe86" binding="myPanel" layout-manager="GridLayoutManager" row-count="6" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="6">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="74" y="134" width="245" height="152"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="9636d" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myClassChooser">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="c2ef3" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myFieldChooser">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<xy id="3cfba" layout-manager="XYLayout" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="5" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="2" fill="1" indent="0" use-parent-layout="false">
<minimum-size width="-1" height="1"/>
<maximum-size width="-1" height="1"/>
</grid>
</constraints>
<properties/>
<border type="bevel-raised"/>
<children/>
</xy>
<vspacer id="339be">
<constraints>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="e8c64" class="javax.swing.JLabel">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="messages/DebuggerBundle" key="label.add.field.breakpoint.dialog.field.name"/>
</properties>
</component>
<component id="c159b" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="messages/DebuggerBundle" key="label.add.field.breakpoint.dialog.fq.name"/>
</properties>
</component>
</children>
</grid>
</form>
@@ -0,0 +1,146 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints.dialog;
import com.intellij.debugger.DebuggerBundle;
import com.intellij.ide.util.MemberChooser;
import com.intellij.ide.util.TreeClassChooser;
import com.intellij.ide.util.TreeClassChooserFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.idea.core.util.DescriptorMemberChooserObject;
import org.jetbrains.kotlin.idea.core.util.UiUtilsKt;
import org.jetbrains.kotlin.psi.KtProperty;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
public abstract class AddFieldBreakpointDialog extends DialogWrapper {
private final Project myProject;
private JPanel myPanel;
private TextFieldWithBrowseButton myFieldChooser;
private TextFieldWithBrowseButton myClassChooser;
public AddFieldBreakpointDialog(Project project) {
super(project, true);
myProject = project;
setTitle(DebuggerBundle.message("add.field.breakpoint.dialog.title"));
init();
}
@Override
protected JComponent createCenterPanel() {
UiUtilsKt.onTextChange(
myClassChooser.getTextField(),
(DocumentEvent e) -> {
updateUI();
return Unit.INSTANCE;
}
);
myClassChooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
PsiClass currentClass = getSelectedClass();
TreeClassChooser chooser = TreeClassChooserFactory.getInstance(myProject).createAllProjectScopeChooser(
DebuggerBundle.message("add.field.breakpoint.dialog.classchooser.title"));
if (currentClass != null) {
PsiFile containingFile = currentClass.getContainingFile();
if (containingFile != null) {
PsiDirectory containingDirectory = containingFile.getContainingDirectory();
if (containingDirectory != null) {
chooser.selectDirectory(containingDirectory);
}
}
}
chooser.showDialog();
PsiClass selectedClass = chooser.getSelected();
if (selectedClass != null) {
myClassChooser.setText(selectedClass.getQualifiedName());
}
}
});
myFieldChooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
PsiClass selectedClass = getSelectedClass();
DescriptorMemberChooserObject[] properties = FieldBreakpointDialogUtilKt.collectProperties(selectedClass);
MemberChooser<DescriptorMemberChooserObject> chooser = new MemberChooser<DescriptorMemberChooserObject>(properties, false, false, myProject);
chooser.setTitle(DebuggerBundle.message("add.field.breakpoint.dialog.field.chooser.title", properties.length));
chooser.setCopyJavadocVisible(false);
chooser.show();
List<DescriptorMemberChooserObject> selectedElements = chooser.getSelectedElements();
if (selectedElements != null && selectedElements.size() == 1) {
KtProperty field = (KtProperty) selectedElements.get(0).getElement();
myFieldChooser.setText(field.getName());
}
}
});
myFieldChooser.setEnabled(false);
return myPanel;
}
private void updateUI() {
PsiClass selectedClass = getSelectedClass();
myFieldChooser.setEnabled(selectedClass != null);
}
private PsiClass getSelectedClass() {
PsiManager psiManager = PsiManager.getInstance(myProject);
String classQName = myClassChooser.getText();
if (classQName == null || classQName.isEmpty()) {
return null;
}
return JavaPsiFacade.getInstance(psiManager.getProject()).findClass(classQName, GlobalSearchScope.allScope(myProject));
}
@Override
public JComponent getPreferredFocusedComponent() {
return myClassChooser.getTextField();
}
public String getClassName() {
return myClassChooser.getText();
}
@Override
protected String getDimensionServiceKey() {
return "#com.intellij.debugger.ui.breakpoints.BreakpointsConfigurationDialogFactory.BreakpointsConfigurationDialog.AddFieldBreakpointDialog";
}
public String getFieldName() {
return myFieldChooser.getText();
}
protected abstract boolean validateData();
@Override
protected void doOKAction() {
if (validateData()) {
super.doOKAction();
}
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.breakpoints.dialog
import com.intellij.psi.PsiClass
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor
import org.jetbrains.kotlin.idea.core.util.DescriptorMemberChooserObject
import org.jetbrains.kotlin.psi.KtProperty
fun PsiClass.collectProperties(): Array<DescriptorMemberChooserObject> {
if (this is KtLightClassForFacade) {
val result = arrayListOf<DescriptorMemberChooserObject>()
this.files.forEach {
it.declarations.filterIsInstance<KtProperty>().forEach {
result.add(DescriptorMemberChooserObject(it, it.unsafeResolveToDescriptor()))
}
}
return result.toTypedArray()
}
if (this is KtLightClass) {
val origin = this.kotlinOrigin
if (origin != null) {
return origin.declarations.filterIsInstance<KtProperty>().map {
DescriptorMemberChooserObject(it, it.unsafeResolveToDescriptor())
}.toTypedArray()
}
}
return emptyArray()
}
@@ -0,0 +1,256 @@
/*
* Copyright 2010-2018 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.idea.debugger
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.psi.PsiElement
import com.sun.jdi.*
import com.sun.tools.jdi.LocalVariableImpl
import org.jetbrains.kotlin.codegen.binding.CodegenBinding
import org.jetbrains.kotlin.codegen.binding.CodegenBinding.asmTypeForAnonymousClass
import org.jetbrains.kotlin.codegen.coroutines.DO_RESUME_METHOD_NAME
import org.jetbrains.kotlin.codegen.coroutines.INVOKE_SUSPEND_METHOD_NAME
import org.jetbrains.kotlin.codegen.coroutines.continuationAsmTypes
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.core.util.getLineEndOffset
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.org.objectweb.asm.Type as AsmType
import java.util.*
fun Location.isInKotlinSources(): Boolean {
val declaringType = declaringType()
val fileExtension = declaringType.safeSourceName()?.substringAfterLast('.')?.toLowerCase() ?: ""
return fileExtension in KotlinFileTypeFactory.KOTLIN_EXTENSIONS || declaringType.containsKotlinStrata()
}
fun ReferenceType.containsKotlinStrata() = availableStrata().contains(KOTLIN_STRATA_NAME)
fun isInsideInlineArgument(
inlineArgument: KtFunction,
location: Location,
debugProcess: DebugProcessImpl,
bindingContext: BindingContext = KotlinDebuggerCaches.getOrCreateTypeMapper(inlineArgument).bindingContext
): Boolean {
val visibleVariables = location.visibleVariables(debugProcess)
val markerLocalVariables = visibleVariables.filter { it.name().startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT) }
val context = KotlinDebuggerCaches.getOrCreateTypeMapper(inlineArgument).bindingContext
val lambdaOrdinal = runReadAction { lambdaOrdinalByArgument(inlineArgument, context) }
val functionName = runReadAction { functionNameByArgument(inlineArgument, context) }
return markerLocalVariables
.map { it.name().drop(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT.length) }
.any { variableName ->
if (variableName.startsWith("-")) {
val lambdaClassName = asmTypeForAnonymousClass(bindingContext, inlineArgument)
.internalName.substringAfterLast("/")
variableName == "-$functionName-$lambdaClassName"
} else {
// For Kotlin up to 1.3.10
lambdaOrdinalByLocalVariable(variableName) == lambdaOrdinal
&& functionNameByLocalVariable(variableName) == functionName
}
}
}
fun <T : Any> DebugProcessImpl.invokeInManagerThread(f: (DebuggerContextImpl) -> T?): T? {
var result: T? = null
val command: DebuggerCommandImpl = object : DebuggerCommandImpl() {
override fun action() {
result = runReadAction { f(debuggerContext) }
}
}
when {
DebuggerManagerThreadImpl.isManagerThread() ->
managerThread.invoke(command)
else ->
managerThread.invokeAndWait(command)
}
return result
}
private fun lambdaOrdinalByArgument(elementAt: KtFunction, context: BindingContext): Int {
val type = CodegenBinding.asmTypeForAnonymousClass(context, elementAt)
return type.className.substringAfterLast("$").toInt()
}
private fun functionNameByArgument(elementAt: KtFunction, context: BindingContext): String {
val inlineArgumentDescriptor = InlineUtil.getInlineArgumentDescriptor(elementAt, context)
return inlineArgumentDescriptor?.containingDeclaration?.name?.asString() ?: "unknown"
}
private fun Location.visibleVariables(debugProcess: DebugProcessImpl): List<LocalVariable> {
val stackFrame = MockStackFrame(this, debugProcess.virtualMachineProxy.virtualMachine)
return stackFrame.visibleVariables()
}
private fun lambdaOrdinalByLocalVariable(name: String): Int {
try {
val nameWithoutPrefix = name.removePrefix(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT)
return Integer.parseInt(nameWithoutPrefix.substringBefore("$", nameWithoutPrefix))
}
catch(e: NumberFormatException) {
return 0
}
}
private fun functionNameByLocalVariable(name: String): String {
val nameWithoutPrefix = name.removePrefix(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT)
return nameWithoutPrefix.substringAfterLast("$", "unknown")
}
private class MockStackFrame(private val location: Location, private val vm: VirtualMachine) : StackFrame {
private var visibleVariables: Map<String, LocalVariable>? = null
override fun location() = location
override fun thread() = null
override fun thisObject() = null
private fun createVisibleVariables() {
if (visibleVariables == null) {
val allVariables = location.method().safeVariables() ?: emptyList()
val map = HashMap<String, LocalVariable>(allVariables.size)
for (allVariable in allVariables) {
val variable = allVariable as LocalVariableImpl
val name = variable.name()
if (variable.isVisible(this)) {
map.put(name, variable)
}
}
visibleVariables = map
}
}
override fun visibleVariables(): List<LocalVariable> {
createVisibleVariables()
val mapAsList = ArrayList(visibleVariables!!.values)
Collections.sort(mapAsList)
return mapAsList
}
override fun visibleVariableByName(name: String): LocalVariable? {
createVisibleVariables()
return visibleVariables!![name]
}
override fun getValue(variable: LocalVariable) = null
override fun getValues(variables: List<LocalVariable>): Map<LocalVariable, Value> = emptyMap()
override fun setValue(variable: LocalVariable, value: Value) {
}
override fun getArgumentValues(): List<Value> = emptyList()
override fun virtualMachine() = vm
}
private const val DO_RESUME_SIGNATURE = "(Ljava/lang/Object;Ljava/lang/Throwable;)Ljava/lang/Object;"
private const val INVOKE_SUSPEND_SIGNATURE = "(Ljava/lang/Object;)Ljava/lang/Object;"
fun isInSuspendMethod(location: Location): Boolean {
val method = location.method()
val signature = method.signature()
for (continuationAsmType in continuationAsmTypes()) {
if (signature.contains(continuationAsmType.toString()) ||
(method.name() == DO_RESUME_METHOD_NAME && signature == DO_RESUME_SIGNATURE) ||
(method.name() == INVOKE_SUSPEND_METHOD_NAME && signature == INVOKE_SUSPEND_SIGNATURE)
) return true
}
return false
}
fun suspendFunctionFirstLineLocation(location: Location): Int? {
if (!isInSuspendMethod(location)) {
return null
}
val lineNumber = location.method().location()?.lineNumber()
if (lineNumber == -1) {
return null
}
return lineNumber
}
fun isOnSuspendReturnOrReenter(location: Location): Boolean {
val suspendStartLineNumber = suspendFunctionFirstLineLocation(location) ?: return false
return suspendStartLineNumber == location.lineNumber()
}
fun isLastLineLocationInMethod(location: Location): Boolean {
val knownLines = location.method().safeAllLineLocations().map { it.lineNumber() }.filter { it != -1 }
if (knownLines.isEmpty()) {
return false
}
return knownLines.max() == location.lineNumber()
}
fun isOneLineMethod(location: Location): Boolean {
val allLineLocations = location.method().safeAllLineLocations()
val firstLine = allLineLocations.firstOrNull()?.lineNumber()
val lastLine = allLineLocations.lastOrNull()?.lineNumber()
return firstLine != null && firstLine == lastLine
}
fun findElementAtLine(file: KtFile, line: Int): PsiElement? {
val lineStartOffset = file.getLineStartOffset(line) ?: return null
val lineEndOffset = file.getLineEndOffset(line) ?: return null
var topMostElement: PsiElement? = null
var elementAt: PsiElement?
for (offset in lineStartOffset until lineEndOffset) {
elementAt = file.findElementAt(offset)
if (elementAt != null) {
topMostElement = CodeInsightUtils.getTopmostElementAtOffset(elementAt, offset)
if (topMostElement is KtElement) {
break
}
}
}
return topMostElement
}
fun findCallByEndToken(element: PsiElement): KtCallExpression? {
if (element is KtElement) return null
return when (element.node.elementType) {
KtTokens.RPAR -> (element.parent as? KtValueArgumentList)?.parent as? KtCallExpression
KtTokens.RBRACE -> {
val braceParent = CodeInsightUtils.getTopParentWithEndOffset(element, KtCallExpression::class.java)
when (braceParent) {
is KtCallExpression -> braceParent
is KtLambdaArgument -> braceParent.parent as? KtCallExpression
is KtValueArgument -> (braceParent.parent as? KtValueArgumentList)?.parent as? KtCallExpression
else -> null
}
}
else -> null
}
}
val DebuggerContextImpl.canRunEvaluation: Boolean
get() = debugProcess?.canRunEvaluation ?: false
val DebugProcessImpl.canRunEvaluation: Boolean
get() = suspendManager.pausedContext != null
@@ -0,0 +1,102 @@
/*
* Copyright 2000-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.idea.debugger.evaluate
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.ContextUtil
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.evaluation.expression.ExpressionEvaluator
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.ui.EditorEvaluationCommand
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.CommonClassNames
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.ClassType
import com.sun.jdi.Value
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.debugger.getClassDescriptor
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.org.objectweb.asm.Type as AsmType
abstract class KotlinRuntimeTypeEvaluator(
editor: Editor?,
expression: KtExpression,
context: DebuggerContextImpl,
indicator: ProgressIndicator
) : EditorEvaluationCommand<KotlinType>(editor, expression, context, indicator) {
override fun threadAction() {
var type: KotlinType? = null
try {
type = evaluate()
} catch (ignored: ProcessCanceledException) {
} catch (ignored: EvaluateException) {
} finally {
typeCalculationFinished(type)
}
}
protected abstract fun typeCalculationFinished(type: KotlinType?)
override fun evaluate(evaluationContext: EvaluationContextImpl): KotlinType? {
val project = evaluationContext.project
val evaluator = DebuggerInvocationUtil.commitAndRunReadAction<ExpressionEvaluator>(project) {
val codeFragment = KtPsiFactory(myElement.project)
.createExpressionCodeFragment(myElement.text, myElement.containingFile.context)
val codeFragmentFactory = DebuggerUtilsEx.getCodeFragmentFactory(codeFragment.context, KotlinFileType.INSTANCE)
codeFragmentFactory.evaluatorBuilder.build(codeFragment, ContextUtil.getSourcePosition(evaluationContext))
}
val value = evaluator.evaluate(evaluationContext)
if (value != null) {
return runReadAction { getCastableRuntimeType(evaluationContext.debugProcess.searchScope, value) }
}
throw EvaluateExceptionUtil.createEvaluateException(DebuggerBundle.message("evaluation.error.surrounded.expression.null"))
}
companion object {
private fun getCastableRuntimeType(scope: GlobalSearchScope, value: Value): KotlinType? {
val myValue = value.asValue()
var psiClass = myValue.asmType.getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
val type = value.type()
if (type is ClassType) {
val superclass = type.superclass()
if (superclass != null && CommonClassNames.JAVA_LANG_OBJECT != superclass.name()) {
psiClass = AsmType.getType(superclass.signature()).getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
}
for (interfaceType in type.interfaces()) {
psiClass = AsmType.getType(interfaceType.signature()).getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
}
}
return null
}
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.filter
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.ui.classFilter.ClassFilter
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings
private val KOTLIN_STDLIB_FILTER = "kotlin.*"
fun addKotlinStdlibDebugFilterIfNeeded() {
if (!KotlinDebuggerSettings.getInstance().DEBUG_IS_FILTER_FOR_STDLIB_ALREADY_ADDED) {
val settings = DebuggerSettings.getInstance()!!
val newFilters = (settings.steppingFilters + ClassFilter(KOTLIN_STDLIB_FILTER))
settings.steppingFilters = newFilters
KotlinDebuggerSettings.getInstance().DEBUG_IS_FILTER_FOR_STDLIB_ALREADY_ADDED = true
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.filter
import com.intellij.ui.classFilter.ClassFilter
import com.intellij.ui.classFilter.DebuggerClassFilterProvider
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings
private val FILTERS = listOf(
ClassFilter("kotlin.jvm*"),
ClassFilter("kotlin.reflect*"),
ClassFilter("kotlin.NoWhenBranchMatchedException"),
ClassFilter("kotlin.TypeCastException"),
ClassFilter("kotlin.KotlinNullPointerException")
)
class KotlinDebuggerInternalClassesFilterProvider : DebuggerClassFilterProvider {
override fun getFilters(): List<ClassFilter>? {
return if (KotlinDebuggerSettings.getInstance().DEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES) FILTERS else listOf()
}
}
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.filter
import com.intellij.debugger.engine.SyntheticTypeComponentProvider
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.FqNameUnsafe
import org.jetbrains.org.objectweb.asm.Opcodes
import kotlin.jvm.internal.FunctionReference
import kotlin.jvm.internal.PropertyReference
class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvider {
override fun isSynthetic(typeComponent: TypeComponent?): Boolean {
if (typeComponent !is Method) return false
val containingType = typeComponent.declaringType()
val typeName = containingType.name()
if (!FqNameUnsafe.isValid(typeName)) return false
// TODO: this is most likely not necessary since KT-28453 is fixed, but still can be useful when debugging old compiled code
if (containingType.isCallableReferenceSyntheticClass()) {
return true
}
try {
if (typeComponent.isDelegateToDefaultInterfaceImpl()) return true
if (typeComponent.location()?.lineNumber() != 1) return false
if (typeComponent.allLineLocations().any { it.lineNumber() != 1 }) {
return false
}
return !typeComponent.declaringType().allLineLocations().any { it.lineNumber() != 1 }
}
catch(e: AbsentInformationException) {
return false
}
catch(e: UnsupportedOperationException) {
return false
}
}
private tailrec fun ReferenceType?.isCallableReferenceSyntheticClass(): Boolean {
if (this !is ClassType) return false
val superClass = this.superclass() ?: return false
val superClassName = superClass.name()
if (superClassName == PropertyReference::class.java.name || superClassName == FunctionReference::class.java.name) {
return true
}
// The direct supertype may be PropertyReference0 or something
return if (superClassName.startsWith("kotlin.jvm.internal."))
superClass.isCallableReferenceSyntheticClass()
else
false
}
private fun Method.isDelegateToDefaultInterfaceImpl(): Boolean {
if (safeAllLineLocations().size != 1) return false
if (!virtualMachine().canGetBytecodes()) return false
if (!hasOnlyInvokeStatic(this)) return false
return hasInterfaceWithImplementation(this)
}
private val LOAD_INSTRUCTIONS_WITH_INDEX = Opcodes.ILOAD.toByte()..Opcodes.ALOAD.toByte()
private val LOAD_INSTRUCTIONS = (Opcodes.ALOAD + 1).toByte()..(Opcodes.IALOAD - 1).toByte()
private val RETURN_INSTRUCTIONS = Opcodes.IRETURN.toByte()..Opcodes.RETURN.toByte()
// Check that method contains only load and invokeStatic instructions. Note that if after load goes ldc instruction it could be checkParametersNotNull method invocation
private fun hasOnlyInvokeStatic(m: Method): Boolean {
val bytecodes = m.bytecodes()
var i = 0
var isALoad0BeforeStaticCall = false
while (i < bytecodes.size) {
val instr = bytecodes[i]
when {
instr == 42.toByte() /* ALOAD_0 */ -> {
i += 1
isALoad0BeforeStaticCall = true
}
instr in LOAD_INSTRUCTIONS_WITH_INDEX || instr in LOAD_INSTRUCTIONS -> {
i += 1
if (instr in LOAD_INSTRUCTIONS_WITH_INDEX) i += 1
val nextInstr = bytecodes[i]
if (nextInstr == Opcodes.LDC.toByte()) {
i += 2
isALoad0BeforeStaticCall = false
}
}
instr == Opcodes.INVOKESTATIC.toByte() -> {
i += 3
if (isALoad0BeforeStaticCall && i == (bytecodes.size - 1)) {
val nextInstr = bytecodes[i]
return nextInstr in RETURN_INSTRUCTIONS
}
}
else -> return false
}
}
return false
}
// TODO: class DefaultImpl can be not loaded
private fun hasInterfaceWithImplementation(method: Method): Boolean {
val declaringType = method.declaringType() as? ClassType ?: return false
val interfaces = declaringType.allInterfaces()
val vm = declaringType.virtualMachine()
val traitImpls = interfaces.flatMap { vm.classesByName(it.name() + JvmAbi.DEFAULT_IMPLS_SUFFIX) }
return traitImpls.any { !it.methodsByName(method.name()).isEmpty() }
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.render
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.ui.impl.watch.FieldDescriptorImpl
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiExpression
import com.sun.jdi.*
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.Name
class DelegatedPropertyFieldDescriptor(
project: Project,
objectRef: ObjectReference,
val delegate: Field,
private val renderDelegatedProperty: Boolean
) : FieldDescriptorImpl(project, objectRef, delegate) {
override fun calcValue(evaluationContext: EvaluationContextImpl?): Value? {
if (evaluationContext == null) return null
if (!renderDelegatedProperty) return super.calcValue(evaluationContext)
val method = findGetterForDelegatedProperty()
val threadReference = evaluationContext.suspendContext.thread?.threadReference
if (method == null || threadReference == null) {
return super.calcValue(evaluationContext)
}
try {
return evaluationContext.debugProcess.invokeInstanceMethod(
evaluationContext,
`object`,
method,
listOf<Nothing>(),
evaluationContext.suspendContext.suspendPolicy
)
}
catch(e: EvaluateException) {
return e.exceptionFromTargetVM
}
}
override fun getName(): String {
return delegate.name().removeSuffix(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX)
}
override fun getDescriptorEvaluation(context: DebuggerContext?): PsiExpression? {
return null
}
private fun findGetterForDelegatedProperty(): Method? {
val fieldName = name
if (!Name.isValidIdentifier(fieldName)) return null
return `object`.referenceType().methodsByName(JvmAbi.getterName(fieldName))?.firstOrNull()
}
override fun getDeclaredType(): String? {
val getter = findGetterForDelegatedProperty() ?: return null
val returnType = try {
getter.returnType()
} catch (e: ClassNotLoadedException) {
// Behavior copied from LocalVariableDescriptorImpl (in platform)
return "<unknown>"
}
return returnType?.name()
}
}
@@ -0,0 +1,141 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.render
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.settings.NodeRendererSettings
import com.intellij.debugger.ui.impl.watch.MessageDescriptor
import com.intellij.debugger.ui.impl.watch.NodeManagerImpl
import com.intellij.debugger.ui.tree.DebuggerTreeNode
import com.intellij.debugger.ui.tree.ValueDescriptor
import com.intellij.debugger.ui.tree.render.ChildrenBuilder
import com.intellij.debugger.ui.tree.render.ClassRenderer
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
import com.intellij.openapi.diagnostic.Logger
import com.intellij.xdebugger.settings.XDebuggerSettingsManager
import com.sun.jdi.*
import com.sun.jdi.Type
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings
import org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState
import org.jetbrains.kotlin.idea.debugger.canRunEvaluation
import org.jetbrains.kotlin.load.java.JvmAbi
import java.util.*
import com.sun.jdi.Type as JdiType
import org.jetbrains.org.objectweb.asm.Type as AsmType
private val LOG = Logger.getInstance(KotlinClassWithDelegatedPropertyRenderer::class.java)
private fun notPreparedClassMessage(referenceType: ReferenceType) =
"$referenceType ${referenceType.isPrepared} ${referenceType.sourceName()}"
class KotlinClassWithDelegatedPropertyRenderer : ClassRenderer() {
private val rendererSettings = NodeRendererSettings.getInstance()
override fun isApplicable(jdiType: Type?): Boolean {
if (!super.isApplicable(jdiType)) return false
if (jdiType !is ReferenceType) return false
if (!jdiType.isPrepared) {
LOG.info(notPreparedClassMessage(jdiType))
return false
}
try {
return jdiType.allFields().any { it.name().endsWith(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX) }
} catch (notPrepared: ClassNotPreparedException) {
LOG.error(notPreparedClassMessage(jdiType), notPrepared)
}
return false
}
override fun calcLabel(
descriptor: ValueDescriptor,
evaluationContext: EvaluationContext,
listener: DescriptorLabelListener
): String {
val res = calcToStringLabel(descriptor, evaluationContext, listener)
if (res != null) {
return res
}
return super.calcLabel(descriptor, evaluationContext, listener)
}
private fun calcToStringLabel(
descriptor: ValueDescriptor, evaluationContext: EvaluationContext,
listener: DescriptorLabelListener
): String? {
val toStringRenderer = rendererSettings.toStringRenderer
if (toStringRenderer.isEnabled && DebuggerManagerEx.getInstanceEx(evaluationContext.project).context.canRunEvaluation) {
if (toStringRenderer.isApplicable(descriptor.type)) {
return toStringRenderer.calcLabel(descriptor, evaluationContext, listener)
}
}
return null
}
override fun buildChildren(value: Value?, builder: ChildrenBuilder, context: EvaluationContext) {
DebuggerManagerThreadImpl.assertIsManagerThread()
if (value !is ObjectReference) return
val nodeManager = builder.nodeManager!!
val nodeDescriptorFactory = builder.descriptorManager!!
val fields = value.referenceType().allFields()
if (fields.isEmpty()) {
builder.setChildren(listOf(nodeManager.createMessageNode(MessageDescriptor.CLASS_HAS_NO_FIELDS.label)))
return
}
val children = ArrayList<DebuggerTreeNode>()
for (field in fields) {
if (!shouldDisplay(context, value, field)) {
continue
}
val fieldDescriptor = nodeDescriptorFactory.getFieldDescriptor(builder.parentDescriptor, value, field)
if (field.name().endsWith(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX)) {
val shouldRenderDelegatedProperty = KotlinDebuggerSettings.getInstance().DEBUG_RENDER_DELEGATED_PROPERTIES
if (shouldRenderDelegatedProperty && !ToggleKotlinVariablesState.getService().kotlinVariableView) {
children.add(nodeManager.createNode(fieldDescriptor, context))
}
val delegatedPropertyDescriptor = DelegatedPropertyFieldDescriptor(
context.debugProcess.project!!,
value,
field,
shouldRenderDelegatedProperty
)
children.add(nodeManager.createNode(delegatedPropertyDescriptor, context))
} else {
children.add(nodeManager.createNode(fieldDescriptor, context))
}
}
if (XDebuggerSettingsManager.getInstance()!!.dataViewSettings.isSortValues) {
children.sortedWith(NodeManagerImpl.getNodeComparator())
}
builder.setChildren(children)
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.RequestHint;
import com.intellij.debugger.engine.SuspendContextImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.sun.jdi.request.StepRequest;
import org.jetbrains.annotations.NotNull;
public class DebugProcessImplHelper {
public static DebugProcessImpl.StepOverCommand createStepOverCommandWithCustomFilter(
SuspendContextImpl suspendContext,
boolean ignoreBreakpoints,
KotlinSuspendCallStepOverFilter methodFilter
) {
DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
return debugProcess.new StepOverCommand(suspendContext, ignoreBreakpoints, StepRequest.STEP_LINE) {
@NotNull
@Override
protected RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread) {
@SuppressWarnings("MagicConstant")
RequestHint hint = new RequestHintWithMethodFilter(stepThread, suspendContext, StepRequest.STEP_OVER, methodFilter);
hint.setRestoreBreakpoints(ignoreBreakpoints);
hint.setIgnoreFilters(ignoreBreakpoints || debugProcess.getSession().shouldIgnoreSteppingFilters());
return hint;
}
};
}
}
@@ -0,0 +1,211 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.SuspendContextImpl;
import com.intellij.debugger.engine.evaluation.EvaluateException;
import com.intellij.debugger.impl.DebuggerUtilsEx;
import com.intellij.debugger.jdi.StackFrameProxyImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.ui.classFilter.DebuggerClassFilterProvider;
import com.sun.jdi.Location;
import com.sun.jdi.ObjectCollectedException;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.StepRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.debugger.NoStrataPositionManagerHelperKt;
import org.jetbrains.kotlin.psi.KtFunctionLiteral;
import org.jetbrains.kotlin.psi.KtNamedFunction;
import java.util.ArrayList;
import java.util.List;
public class DebuggerSteppingHelper {
private static Logger LOG = Logger.getInstance(DebuggerSteppingHelper.class);
public static DebugProcessImpl.ResumeCommand createStepOverCommand(
final SuspendContextImpl suspendContext,
final boolean ignoreBreakpoints,
final KotlinSteppingCommandProvider.KotlinSourcePosition kotlinSourcePosition
) {
final DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
return debugProcess.new ResumeCommand(suspendContext) {
@Override
public void contextAction() {
boolean isDexDebug = NoStrataPositionManagerHelperKt.isDexDebug(suspendContext.getDebugProcess());
try {
StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
if (frameProxy != null) {
Action action = KotlinSteppingCommandProviderKt.getStepOverAction(
frameProxy.location(),
kotlinSourcePosition,
frameProxy,
isDexDebug
);
createStepRequest(
suspendContext, getContextThread(),
debugProcess.getVirtualMachineProxy().eventRequestManager(),
StepRequest.STEP_LINE, StepRequest.STEP_OUT);
action.apply(debugProcess, suspendContext, ignoreBreakpoints);
return;
}
debugProcess.createStepOutCommand(suspendContext).contextAction();
}
catch (EvaluateException e) {
LOG.error(e);
}
}
};
}
public static DebugProcessImpl.ResumeCommand createStepOutCommand(
final SuspendContextImpl suspendContext,
final boolean ignoreBreakpoints,
final List<KtNamedFunction> inlineFunctions,
final KtFunctionLiteral inlineArgument
) {
final DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
return debugProcess.new ResumeCommand(suspendContext) {
@Override
public void contextAction() {
try {
StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
if (frameProxy != null) {
Action action = KotlinSteppingCommandProviderKt.getStepOutAction(
frameProxy.location(),
suspendContext,
inlineFunctions,
inlineArgument
);
createStepRequest(
suspendContext, getContextThread(),
debugProcess.getVirtualMachineProxy().eventRequestManager(),
StepRequest.STEP_LINE, StepRequest.STEP_OUT);
action.apply(debugProcess, suspendContext, ignoreBreakpoints);
return;
}
debugProcess.createStepOverCommand(suspendContext, ignoreBreakpoints).contextAction();
}
catch (EvaluateException ignored) {
}
}
};
}
// copied from DebugProcessImpl.doStep
private static void createStepRequest(
@NotNull SuspendContextImpl suspendContext,
@Nullable ThreadReferenceProxyImpl stepThread,
@NotNull EventRequestManager requestManager,
int size, int depth
) {
if (stepThread == null) {
return;
}
try {
ThreadReference stepThreadReference = stepThread.getThreadReference();
requestManager.deleteEventRequests(requestManager.stepRequests());
StepRequest stepRequest = requestManager.createStepRequest(stepThreadReference, size, depth);
List<ClassFilter> activeFilters = getActiveFilters();
if (!activeFilters.isEmpty()) {
String currentClassName = getCurrentClassName(stepThread);
if (currentClassName == null || !DebuggerUtilsEx.isFiltered(currentClassName, activeFilters)) {
// add class filters
for (ClassFilter filter : activeFilters) {
stepRequest.addClassExclusionFilter(filter.getPattern());
}
}
}
// suspend policy to match the suspend policy of the context:
// if all threads were suspended, then during stepping all the threads must be suspended
// if only event thread were suspended, then only this particular thread must be suspended during stepping
stepRequest.setSuspendPolicy(suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD
? EventRequest.SUSPEND_EVENT_THREAD
: EventRequest.SUSPEND_ALL);
stepRequest.enable();
}
catch (ObjectCollectedException ignored) {
}
}
// copied from DebugProcessImpl.getActiveFilters
@NotNull
private static List<ClassFilter> getActiveFilters() {
List<ClassFilter> activeFilters = new ArrayList<ClassFilter>();
DebuggerSettings settings = DebuggerSettings.getInstance();
if (settings.TRACING_FILTERS_ENABLED) {
for (ClassFilter filter : settings.getSteppingFilters()) {
if (filter.isEnabled()) {
activeFilters.add(filter);
}
}
}
for (DebuggerClassFilterProvider provider : Extensions.getExtensions(DebuggerClassFilterProvider.EP_NAME)) {
for (ClassFilter filter : provider.getFilters()) {
if (filter.isEnabled()) {
activeFilters.add(filter);
}
}
}
return activeFilters;
}
// copied from DebugProcessImpl.getActiveFilters
@Nullable
private static String getCurrentClassName(ThreadReferenceProxyImpl thread) {
try {
if (thread != null && thread.frameCount() > 0) {
StackFrameProxyImpl stackFrame = thread.frame(0);
if (stackFrame != null) {
Location location = stackFrame.location();
ReferenceType referenceType = location == null ? null : location.declaringType();
if (referenceType != null) {
return referenceType.name();
}
}
}
}
catch (EvaluateException ignored) {
}
return null;
}
}
@@ -0,0 +1,95 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.NamedMethodFilter
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.util.Range
import com.sun.jdi.Location
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.DECLARATION
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypesAndPredicate
import org.jetbrains.kotlin.resolve.DescriptorUtils
class KotlinBasicStepMethodFilter(
private val declarationPtr: SmartPsiElementPointer<KtDeclaration>?,
private val isInvoke: Boolean,
private val targetMethodName: String,
private val myCallingExpressionLines: Range<Int>
) : NamedMethodFilter {
init {
assert(declarationPtr != null || isInvoke)
}
override fun getCallingExpressionLines() = myCallingExpressionLines
override fun getMethodName() = targetMethodName
override fun locationMatches(process: DebugProcessImpl, location: Location): Boolean {
val method = location.method()
if (targetMethodName != method.name()) return false
val positionManager = process.positionManager
val (currentDescriptor, currentDeclaration) = runReadAction {
val elementAt = positionManager.getSourcePosition(location)?.elementAt
val declaration = elementAt?.getParentOfTypesAndPredicate(false, KtDeclaration::class.java) {
it !is KtProperty || !it.isLocal
}
if (declaration is KtClass && method.name() == "<init>") {
declaration.resolveToDescriptorIfAny()?.unsubstitutedPrimaryConstructor to declaration
} else {
declaration?.resolveToDescriptorIfAny() to declaration
}
}
if (currentDescriptor == null || currentDeclaration == null) {
return false
}
@Suppress("FoldInitializerAndIfToElvis")
if (currentDescriptor !is CallableMemberDescriptor) return false
if (currentDescriptor.kind != DECLARATION) return false
if (isInvoke) {
// There can be only one 'invoke' target at the moment so consider position as expected.
// Descriptors can be not-equal, say when parameter has type `(T) -> T` and lambda is `Int.() -> Int`.
return true
}
val declaration = declarationPtr?.element
?: return true // Element is lost. But we know that name is matches, so stop.
if (currentDeclaration.isEquivalentTo(declaration)) {
return true
}
return DescriptorUtils.getAllOverriddenDescriptors(currentDescriptor).any { baseOfCurrent ->
val currentBaseDeclaration = DescriptorToSourceUtilsIde.getAnyDeclaration(currentDeclaration.project, baseOfCurrent)
declaration.isEquivalentTo(currentBaseDeclaration)
}
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.BreakpointStepMethodFilter
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.util.Range
import com.sun.jdi.Location
import org.jetbrains.kotlin.codegen.coroutines.isResumeImplMethodNameFromAnyLanguageSettings
import org.jetbrains.kotlin.idea.core.util.isMultiLine
import org.jetbrains.kotlin.idea.debugger.isInsideInlineArgument
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.util.OperatorNameConventions
class KotlinLambdaMethodFilter(
private val lambda: KtFunction,
private val myCallingExpressionLines: Range<Int>,
private val isInline: Boolean,
private val isSuspend: Boolean
) : BreakpointStepMethodFilter {
private val myFirstStatementPosition: SourcePosition?
private val myLastStatementLine: Int
init {
val body = lambda.bodyExpression
if (body != null && lambda.isMultiLine()) {
var firstStatementPosition: SourcePosition? = null
var lastStatementPosition: SourcePosition? = null
val statements = (body as? KtBlockExpression)?.statements ?: listOf(body)
if (statements.isNotEmpty()) {
firstStatementPosition = SourcePosition.createFromElement(statements.first())
if (firstStatementPosition != null) {
val lastStatement = statements.last()
lastStatementPosition = SourcePosition.createFromOffset(firstStatementPosition.file, lastStatement.textRange.endOffset)
}
}
myFirstStatementPosition = firstStatementPosition
myLastStatementLine = if (lastStatementPosition != null) lastStatementPosition.line else -1
} else {
myFirstStatementPosition = SourcePosition.createFromElement(lambda)
myLastStatementLine = myFirstStatementPosition!!.line
}
}
override fun getBreakpointPosition() = myFirstStatementPosition
override fun getLastStatementLine() = myLastStatementLine
override fun locationMatches(process: DebugProcessImpl, location: Location): Boolean {
val method = location.method()
if (isInline) {
return isInsideInlineArgument(lambda, location, process)
}
return isLambdaName(method.name())
}
override fun getCallingExpressionLines() = if (isInline) Range(0, Int.MAX_VALUE) else myCallingExpressionLines
private fun isLambdaName(name: String?): Boolean {
if (isSuspend && name != null) {
return isResumeImplMethodNameFromAnyLanguageSettings(name)
}
return name == OperatorNameConventions.INVOKE.asString()
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.actions.SmartStepTarget
import com.intellij.util.Range
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.util.OperatorNameConventions
import javax.swing.Icon
class KotlinLambdaSmartStepTarget(
label: String,
highlightElement: KtFunction,
lines: Range<Int>,
val isInline: Boolean,
val isSuspend: Boolean
) : SmartStepTarget(label, highlightElement, true, lines) {
override fun getIcon(): Icon = KotlinIcons.LAMBDA
fun getLambda() = highlightElement as KtFunction
companion object {
fun calcLabel(descriptor: DeclarationDescriptor, paramName: Name): String {
return "${descriptor.name.asString()}: ${paramName.asString()}.${OperatorNameConventions.INVOKE.asString()}()"
}
}
}
@@ -0,0 +1,79 @@
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.actions.SmartStepTarget
import com.intellij.psi.PsiElement
import com.intellij.util.Range
import org.jetbrains.kotlin.builtins.functions.FunctionInvokeDescriptor
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.renderer.ParameterNameRenderingPolicy
import org.jetbrains.kotlin.renderer.PropertyAccessorRenderingPolicy
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension
import javax.swing.Icon
class KotlinMethodSmartStepTarget(
descriptor: CallableMemberDescriptor,
val declaration: KtDeclaration?,
label: String,
highlightElement: PsiElement,
lines: Range<Int>
) : SmartStepTarget(label, highlightElement, false, lines) {
val isInvoke = descriptor is FunctionInvokeDescriptor
init {
assert(declaration != null || isInvoke)
}
private val isExtension = descriptor.isExtension
val targetMethodName: String = when (descriptor) {
is ClassDescriptor, is ConstructorDescriptor -> "<init>"
is PropertyAccessorDescriptor -> JvmAbi.getterName(descriptor.correspondingProperty.name.asString())
else -> descriptor.name.asString()
}
override fun getIcon(): Icon? {
return when {
isExtension -> KotlinIcons.EXTENSION_FUNCTION
else -> KotlinIcons.FUNCTION
}
}
companion object {
private val renderer = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_NO_ANNOTATIONS.withOptions {
parameterNameRenderingPolicy = ParameterNameRenderingPolicy.NONE
withoutReturnType = true
propertyAccessorRenderingPolicy = PropertyAccessorRenderingPolicy.PRETTY
startFromName = true
modifiers = emptySet()
}
fun calcLabel(descriptor: DeclarationDescriptor): String {
return renderer.render(descriptor)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is KotlinMethodSmartStepTarget) return false
if (isInvoke && other.isInvoke) {
// Don't allow to choose several invoke targets in smart step into as we can't distinguish them reliably during debug
return true
}
return declaration === other.declaration
}
override fun hashCode(): Int {
if (isInvoke) {
// Predefined value to make all FunctionInvokeDescriptor targets equal
return 42
}
return declaration!!.hashCode()
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.SimplePropertyGetterProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.*
class KotlinSimpleGetterProvider : SimplePropertyGetterProvider {
override fun isInsideSimpleGetter(element: PsiElement): Boolean {
// class A(val a: Int)
if (element is KtParameter) {
return true
}
val accessor = PsiTreeUtil.getParentOfType(element, KtPropertyAccessor::class.java)
if (accessor != null && accessor.isGetter) {
val body = accessor.bodyExpression
return when (body) {
is KtBlockExpression -> {
// val a: Int get() { return field }
val returnedExpression = (body.statements.singleOrNull() as? KtReturnExpression)?.returnedExpression ?: return false
returnedExpression.textMatches("field")
}
is KtExpression -> body.textMatches("field") // val a: Int get() = field
else -> false
}
}
val property = PsiTreeUtil.getParentOfType(element, KtProperty::class.java)
// val a = foo()
if (property != null) {
return property.getter == null && !property.isLocal
}
return false
}
}
@@ -0,0 +1,251 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.actions.JvmSmartStepIntoHandler
import com.intellij.debugger.actions.MethodSmartStepTarget
import com.intellij.debugger.actions.SmartStepTarget
import com.intellij.debugger.engine.MethodFilter
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiMethod
import com.intellij.util.Range
import com.intellij.util.containers.OrderedSet
import org.jetbrains.kotlin.builtins.functions.FunctionInvokeDescriptor
import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType
import org.jetbrains.kotlin.builtins.isSuspendFunctionType
import org.jetbrains.kotlin.codegen.intrinsics.IntrinsicMethods
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.isFromJava
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getParentCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.inline.InlineUtil
class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() {
override fun isAvailable(position: SourcePosition?) = position?.file is KtFile
override fun findSmartStepTargets(position: SourcePosition): List<SmartStepTarget> {
val file = position.file
val elementAtOffset = position.elementAt ?: return emptyList()
val element = CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, elementAtOffset.textRange.startOffset) as? KtElement
?: return emptyList()
val elementTextRange = element.textRange ?: return emptyList()
val doc = PsiDocumentManager.getInstance(file.project).getDocument(file) ?: return emptyList()
val lines = Range(doc.getLineNumber(elementTextRange.startOffset), doc.getLineNumber(elementTextRange.endOffset))
@Suppress("DEPRECATION")
val bindingContext = element.analyzeWithAllCompilerChecks().bindingContext
val result = OrderedSet<SmartStepTarget>()
// TODO support class initializers, local functions, delegated properties with specified type, setter for properties
element.accept(object : KtTreeVisitorVoid() {
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
recordFunctionLiteral(lambdaExpression.functionLiteral)
}
override fun visitNamedFunction(function: KtNamedFunction) {
if (!recordFunctionLiteral(function)) {
super.visitNamedFunction(function)
}
}
private fun recordFunctionLiteral(function: KtFunction): Boolean {
val context = function.analyze()
val resolvedCall = function.getParentCall(context).getResolvedCall(context)
if (resolvedCall != null) {
val arguments = resolvedCall.valueArguments
for ((param, argument) in arguments) {
if (argument.arguments.any { getArgumentExpression(it) == function }) {
val resultingDescriptor = resolvedCall.resultingDescriptor
val label = KotlinLambdaSmartStepTarget.calcLabel(resultingDescriptor, param.name)
result.add(
KotlinLambdaSmartStepTarget(
label, function, lines, InlineUtil.isInline(resultingDescriptor), param.type.isSuspendFunctionType
)
)
return true
}
}
}
return false
}
private fun getArgumentExpression(it: ValueArgument) =
(it.getArgumentExpression() as? KtLambdaExpression)?.functionLiteral ?: it.getArgumentExpression()
override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) {
// skip calls in object declarations
}
override fun visitIfExpression(expression: KtIfExpression) {
expression.condition?.accept(this)
}
override fun visitWhileExpression(expression: KtWhileExpression) {
expression.condition?.accept(this)
}
override fun visitDoWhileExpression(expression: KtDoWhileExpression) {
expression.condition?.accept(this)
}
override fun visitForExpression(expression: KtForExpression) {
expression.loopRange?.accept(this)
}
override fun visitWhenExpression(expression: KtWhenExpression) {
expression.subjectExpression?.accept(this)
}
override fun visitArrayAccessExpression(expression: KtArrayAccessExpression) {
recordFunction(expression)
super.visitArrayAccessExpression(expression)
}
override fun visitUnaryExpression(expression: KtUnaryExpression) {
recordFunction(expression.operationReference)
super.visitUnaryExpression(expression)
}
override fun visitBinaryExpression(expression: KtBinaryExpression) {
recordFunction(expression.operationReference)
super.visitBinaryExpression(expression)
}
override fun visitCallExpression(expression: KtCallExpression) {
val calleeExpression = expression.calleeExpression
if (calleeExpression != null) {
recordFunction(calleeExpression)
}
super.visitCallExpression(expression)
}
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
recordGetter(expression)
super.visitSimpleNameExpression(expression)
}
private fun recordGetter(expression: KtSimpleNameExpression) {
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
val propertyDescriptor = resolvedCall.resultingDescriptor as? PropertyDescriptor ?: return
val getterDescriptor = propertyDescriptor.getter
if (getterDescriptor == null || getterDescriptor.isDefault) return
val ktDeclaration = DescriptorToSourceUtilsIde.getAnyDeclaration(file.project, getterDescriptor) as? KtDeclaration ?: return
val delegatedResolvedCall = bindingContext[BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, getterDescriptor]
if (delegatedResolvedCall != null) {
val delegatedPropertyGetterDescriptor = delegatedResolvedCall.resultingDescriptor
val label = "${propertyDescriptor.name}." + KotlinMethodSmartStepTarget.calcLabel(delegatedPropertyGetterDescriptor)
result.add(KotlinMethodSmartStepTarget(delegatedPropertyGetterDescriptor, ktDeclaration, label, expression, lines))
} else {
if (ktDeclaration is KtPropertyAccessor && ktDeclaration.hasBody()) {
val label = KotlinMethodSmartStepTarget.calcLabel(getterDescriptor)
result.add(KotlinMethodSmartStepTarget(getterDescriptor, ktDeclaration, label, expression, lines))
}
}
}
private fun recordFunction(expression: KtExpression) {
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
val descriptor = resolvedCall.resultingDescriptor
if (descriptor !is FunctionDescriptor || isIntrinsic(descriptor)) return
val declaration = DescriptorToSourceUtilsIde.getAnyDeclaration(file.project, descriptor)
if (descriptor.isFromJava) {
(declaration as? PsiMethod)?.let {
result.add(MethodSmartStepTarget(it, null, declaration, false, lines))
}
} else {
if (declaration == null && !isInvokeInBuiltinFunction(descriptor)) {
return
}
if (declaration !is KtDeclaration?) return
if (descriptor is ConstructorDescriptor && descriptor.isPrimary) {
if (declaration is KtClass && declaration.getAnonymousInitializers().isEmpty()) {
// There is no constructor or init block, so do not show it in smart step into
return
}
}
val callLabel = KotlinMethodSmartStepTarget.calcLabel(descriptor)
val label = when (descriptor) {
is FunctionInvokeDescriptor -> {
when (expression) {
is KtSimpleNameExpression -> "${runReadAction { expression.text }}.$callLabel"
else -> callLabel
}
}
else -> callLabel
}
result.add(KotlinMethodSmartStepTarget(descriptor, declaration, label, expression, lines))
}
}
}, null)
return result
}
override fun createMethodFilter(stepTarget: SmartStepTarget?): MethodFilter? {
return when (stepTarget) {
is KotlinMethodSmartStepTarget ->
KotlinBasicStepMethodFilter(
stepTarget.declaration?.createSmartPointer(),
stepTarget.isInvoke,
stepTarget.targetMethodName,
stepTarget.callingExpressionLines!!
)
is KotlinLambdaSmartStepTarget ->
KotlinLambdaMethodFilter(
stepTarget.getLambda(), stepTarget.callingExpressionLines!!, stepTarget.isInline, stepTarget.isSuspend
)
else -> super.createMethodFilter(stepTarget)
}
}
private val methods = IntrinsicMethods(JvmTarget.JVM_1_6)
private fun isIntrinsic(descriptor: CallableMemberDescriptor): Boolean {
return methods.getIntrinsic(descriptor) != null
}
private fun isInvokeInBuiltinFunction(descriptor: DeclarationDescriptor): Boolean {
if (descriptor !is FunctionInvokeDescriptor) return false
val classDescriptor = descriptor.containingDeclaration as? ClassDescriptor ?: return false
return classDescriptor.defaultType.isBuiltinFunctionalType
}
}
@@ -0,0 +1,161 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.util.EventDispatcher
import com.sun.jdi.request.EventRequest
import com.sun.jdi.request.StepRequest
import java.lang.reflect.Field
// Mass-copy-paste code for commands behaviour from com.intellij.debugger.engine.DebugProcessImpl
@SuppressWarnings("UnnecessaryFinalOnLocalVariableOrParameter")
class KotlinStepActionFactory(private val debuggerProcess: DebugProcessImpl) {
abstract class KotlinStepAction {
abstract fun contextAction(suspendContext: SuspendContextImpl)
}
fun createKotlinStepOverInlineAction(smartStepFilter: KotlinMethodFilter): KotlinStepAction {
return StepOverInlineCommand(smartStepFilter, StepRequest.STEP_LINE)
}
private val debuggerContext: DebuggerContextImpl get() = debuggerProcess.debuggerContext
private val suspendManager: SuspendManager get() = debuggerProcess.suspendManager
private val project: Project get() = debuggerProcess.project
private val session: DebuggerSession get() = debuggerProcess.session
// TODO: ask for better API
// Should be safe to use reflection as field is protected and not obfuscated
private val debugProcessDispatcher: EventDispatcher<DebugProcessListener> = getFromField("myDebugProcessDispatcher")
// TODO: ask for better API
// Get field by type as it private and obfuscated in Ultimate
private val threadBlockedMonitor: ThreadBlockedMonitor = getFromField(ThreadBlockedMonitor::class.java)
private fun showStatusText(message: String) {
debuggerProcess.showStatusText(message)
}
// TODO: ask for better API
// Should be safe to use reflection as method is protected and not obfuscated
private fun doStep(
suspendContext: SuspendContextImpl,
stepThread: ThreadReferenceProxyImpl,
size: Int, depth: Int, hint: RequestHint
) {
val doStepMethod = DebugProcessImpl::class.java.getDeclaredMethod(
"doStep",
SuspendContextImpl::class.java, ThreadReferenceProxyImpl::class.java,
Integer.TYPE, Integer.TYPE, RequestHint::class.java
)
doStepMethod.isAccessible = true
doStepMethod.invoke(debuggerProcess, suspendContext, stepThread, size, depth, hint)
}
private fun <T> getFromField(fieldType: Class<T>): T {
return getFromField(DebugProcessImpl::class.java.declaredFields.single { it.type == fieldType })
}
private fun <T> getFromField(fieldName: String): T {
return getFromField(DebugProcessImpl::class.java.getDeclaredField(fieldName))
}
private fun <T> getFromField(field: Field?): T {
field!!.isAccessible = true
@Suppress("UNCHECKED_CAST")
return field.get(debuggerProcess) as T
}
private inner class StepOverInlineCommand(private val mySmartStepFilter: KotlinMethodFilter, private val myStepSize: Int) :
KotlinStepAction() {
private fun getContextThread(suspendContext: SuspendContextImpl): ThreadReferenceProxyImpl? {
val contextThread = debuggerContext.threadProxy
return contextThread ?: suspendContext.thread
}
// See: ResumeCommand.applyThreadFilter()
private fun applyThreadFilter(suspendContext: SuspendContextImpl, thread: ThreadReferenceProxyImpl) {
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
// there could be explicit resume as a result of call to voteSuspend()
// e.g. when breakpoint was considered invalid, in that case the filter will be applied _after_
// resuming and all breakpoints in other threads will be ignored.
// As resume() implicitly cleares the filter, the filter must be always applied _before_ any resume() action happens
val breakpointManager = DebuggerManagerEx.getInstanceEx(project).breakpointManager
breakpointManager.applyThreadFilter(debuggerProcess, thread.threadReference)
}
}
// See: StepCommand.resumeAction()
private fun resumeAction(suspendContext: SuspendContextImpl, thread: ThreadReferenceProxyImpl) {
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD || isResumeOnlyCurrentThread) {
threadBlockedMonitor.startWatching(thread)
}
if (isResumeOnlyCurrentThread && suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
suspendManager.resumeThread(suspendContext, thread)
} else {
suspendManager.resume(suspendContext)
}
}
// See: StepIntoCommand.contextAction()
override fun contextAction(suspendContext: SuspendContextImpl) {
showStatusText("Stepping over inline")
val stepThread = getContextThread(suspendContext)
if (stepThread == null) {
// TODO: Intellij code doesn't bother to check thread for null, so probably it's not-null actually
debuggerProcess.createStepOverCommand(suspendContext, true).contextAction(suspendContext)
return
}
val hint = KotlinStepOverInlinedLinesHint(stepThread, suspendContext, mySmartStepFilter)
hint.isResetIgnoreFilters = !session.shouldIgnoreSteppingFilters()
try {
session.setIgnoreStepFiltersFlag(stepThread.frameCount())
} catch (e: EvaluateException) {
LOG.info(e)
}
applyThreadFilter(suspendContext, stepThread)
doStep(suspendContext, stepThread, myStepSize, StepRequest.STEP_OVER, hint)
showStatusText("Process resumed")
resumeAction(suspendContext, stepThread)
debugProcessDispatcher.multicaster.resumed(suspendContext)
}
}
companion object {
private val LOG = Logger.getInstance(KotlinStepActionFactory::class.java)
private val isResumeOnlyCurrentThread: Boolean
get() = DebuggerSettings.getInstance().RESUME_ONLY_CURRENT_THREAD
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.openapi.project.Project
import com.intellij.util.Range
import com.sun.jdi.LocalVariable
import com.sun.jdi.Location
import org.jetbrains.kotlin.idea.debugger.ktLocationInfo
class StepOverFilterData(
val lineNumber: Int,
val stepOverLines: Set<Int>,
val inlineRangeVariables: List<LocalVariable>,
val isDexDebug: Boolean,
val skipAfterCodeIndex: Long = -1
)
class KotlinStepOverInlineFilter(val project: Project, val data: StepOverFilterData) : KotlinMethodFilter {
private fun Location.ktLineNumber() = ktLocationInfo(this, data.isDexDebug, project).first
override fun locationMatches(context: SuspendContextImpl, location: Location): Boolean {
val frameProxy = context.frameProxy ?: return true
if (data.skipAfterCodeIndex != -1L && location.codeIndex() > data.skipAfterCodeIndex) {
return false
}
val currentLine = location.ktLineNumber()
if (!(data.stepOverLines.contains(currentLine))) {
return currentLine != data.lineNumber
}
val visibleInlineVariables = getInlineRangeLocalVariables(frameProxy)
// Our ranges check missed exit from inline function. This is when breakpoint was in last statement of inline functions.
// This can be observed by inline local range-variables. Absence of any means step out was done.
return data.inlineRangeVariables.any { !visibleInlineVariables.contains(it) }
}
override fun locationMatches(process: DebugProcessImpl, location: Location): Boolean {
throw IllegalStateException() // Should not be called from Kotlin hint
}
override fun getCallingExpressionLines(): Range<Int>? = null
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.RequestHint
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.openapi.diagnostic.Logger
import com.sun.jdi.VMDisconnectedException
import com.sun.jdi.request.StepRequest
// Originally copied from RequestHint
class KotlinStepOverInlinedLinesHint(
stepThread: ThreadReferenceProxyImpl,
suspendContext: SuspendContextImpl,
methodFilter: KotlinMethodFilter
) : RequestHint(stepThread, suspendContext, methodFilter) {
private val LOG = Logger.getInstance(KotlinStepOverInlinedLinesHint::class.java)
private val filter = methodFilter
override fun getDepth(): Int = StepRequest.STEP_OVER
override fun getNextStepDepth(context: SuspendContextImpl): Int {
try {
val frameProxy = context.frameProxy
if (frameProxy != null) {
if (isTheSameFrame(context)) {
return if (filter.locationMatches(context, frameProxy.location())) {
STOP
} else {
StepRequest.STEP_OVER
}
}
if (isSteppedOut) {
return STOP
}
return StepRequest.STEP_OUT
}
} catch (ignored: VMDisconnectedException) {
} catch (e: EvaluateException) {
LOG.error(e)
}
return STOP
}
}
@@ -0,0 +1,599 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.NoDataException
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.MethodFilter
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.JvmSteppingCommandProvider
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.psi.PsiElement
import com.intellij.xdebugger.impl.XSourcePositionImpl
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.LocalVariable
import com.sun.jdi.Location
import com.sun.jdi.Method
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.builtins.isFunctionType
import org.jetbrains.kotlin.codegen.inline.KOTLIN_STRATA_NAME
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.GOTO
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.MOVE
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN_OBJECT
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN_VOID
import org.jetbrains.kotlin.idea.debugger.stepping.DexBytecode.RETURN_WIDE
import org.jetbrains.kotlin.idea.core.util.getLineNumber
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.keysToMap
import kotlin.math.max
import kotlin.math.min
class KotlinSteppingCommandProvider : JvmSteppingCommandProvider() {
override fun getStepOverCommand(
suspendContext: SuspendContextImpl?,
ignoreBreakpoints: Boolean,
stepSize: Int
): DebugProcessImpl.ResumeCommand? {
if (suspendContext == null || suspendContext.isResumed) return null
val sourcePosition = suspendContext.debugProcess.debuggerContext.sourcePosition ?: return null
return getStepOverCommand(suspendContext, ignoreBreakpoints, sourcePosition)
}
@TestOnly
fun getStepOverCommand(
suspendContext: SuspendContextImpl,
ignoreBreakpoints: Boolean,
debuggerContext: DebuggerContextImpl
): DebugProcessImpl.ResumeCommand? {
return getStepOverCommand(suspendContext, ignoreBreakpoints, debuggerContext.sourcePosition)
}
private fun getStepOverCommand(
suspendContext: SuspendContextImpl,
ignoreBreakpoints: Boolean,
sourcePosition: SourcePosition
): DebugProcessImpl.ResumeCommand? {
val kotlinSourcePosition = KotlinSourcePosition.create(sourcePosition) ?: return null
if (isSpecialStepOverNeeded(kotlinSourcePosition)) {
return DebuggerSteppingHelper.createStepOverCommand(suspendContext, ignoreBreakpoints, kotlinSourcePosition)
}
val file = sourcePosition.elementAt.containingFile
val location = suspendContext.debugProcess.invokeInManagerThread { suspendContext.frameProxy?.safeLocation() } ?: return null
if (isInSuspendMethod(location) && !isOnSuspendReturnOrReenter(location) && !isLastLineLocationInMethod(location)) {
return DebugProcessImplHelper.createStepOverCommandWithCustomFilter(
suspendContext, ignoreBreakpoints, KotlinSuspendCallStepOverFilter(sourcePosition.line, file, ignoreBreakpoints)
)
}
return null
}
data class KotlinSourcePosition(
val file: KtFile, val function: KtNamedFunction,
val linesRange: IntRange, val sourcePosition: SourcePosition
) {
companion object {
fun create(sourcePosition: SourcePosition): KotlinSourcePosition? {
val file = sourcePosition.file as? KtFile ?: return null
if (sourcePosition.line < 0) return null
val elementAt = sourcePosition.elementAt ?: return null
val containingFunction = elementAt.parents
.filterIsInstance<KtNamedFunction>()
.firstOrNull { !it.isLocal } ?: return null
val startLineNumber = containingFunction.getLineNumber(true) + 1
val endLineNumber = containingFunction.getLineNumber(false) + 1
if (startLineNumber > endLineNumber) return null
val linesRange = startLineNumber..endLineNumber
return KotlinSourcePosition(file, containingFunction, linesRange, sourcePosition)
}
}
}
private fun isSpecialStepOverNeeded(kotlinSourcePosition: KotlinSourcePosition): Boolean {
val sourcePosition = kotlinSourcePosition.sourcePosition
val hasInlineCallsOnLine = getInlineFunctionCallsIfAny(sourcePosition).isNotEmpty()
if (hasInlineCallsOnLine) {
return true
}
// Step over calls to lambda arguments in inline function while execution is already in that function
val containingFunctionDescriptor = kotlinSourcePosition.function.unsafeResolveToDescriptor()
if (InlineUtil.isInline(containingFunctionDescriptor)) {
val inlineArgumentsCallsIfAny = getInlineArgumentsCallsIfAny(sourcePosition, containingFunctionDescriptor)
if (inlineArgumentsCallsIfAny != null && inlineArgumentsCallsIfAny.isNotEmpty()) {
return true
}
}
return false
}
@TestOnly
fun getStepOutCommand(suspendContext: SuspendContextImpl, debugContext: DebuggerContextImpl): DebugProcessImpl.ResumeCommand? {
return getStepOutCommand(suspendContext, debugContext.sourcePosition)
}
override fun getStepOutCommand(suspendContext: SuspendContextImpl?, stepSize: Int): DebugProcessImpl.ResumeCommand? {
if (suspendContext == null || suspendContext.isResumed) return null
val sourcePosition = suspendContext.debugProcess.debuggerContext.sourcePosition ?: return null
return getStepOutCommand(suspendContext, sourcePosition)
}
private fun getStepOutCommand(suspendContext: SuspendContextImpl, sourcePosition: SourcePosition): DebugProcessImpl.ResumeCommand? {
val file = sourcePosition.file as? KtFile ?: return null
if (sourcePosition.line < 0) return null
val lineStartOffset = file.getLineStartOffset(sourcePosition.line) ?: return null
val inlineFunctions = getInlineFunctionsIfAny(file, lineStartOffset)
val inlinedArgument = getInlineArgumentIfAny(sourcePosition.elementAt)
if (inlineFunctions.isEmpty() && inlinedArgument == null) return null
return DebuggerSteppingHelper.createStepOutCommand(suspendContext, true, inlineFunctions, inlinedArgument)
}
}
private fun PsiElement?.contains(element: PsiElement): Boolean {
return this?.textRange?.contains(element.textRange) ?: false
}
private fun getInlineCallFunctionArgumentsIfAny(sourcePosition: SourcePosition): List<KtFunction> {
val inlineFunctionCalls = getInlineFunctionCallsIfAny(sourcePosition)
return getInlineArgumentsIfAny(inlineFunctionCalls)
}
private fun getInlineFunctionsIfAny(file: KtFile, offset: Int): List<KtNamedFunction> {
val elementAt = file.findElementAt(offset) ?: return emptyList()
val containingFunction = elementAt.getParentOfType<KtNamedFunction>(false) ?: return emptyList()
val descriptor = containingFunction.unsafeResolveToDescriptor()
if (!InlineUtil.isInline(descriptor)) return emptyList()
return DebuggerUtils.analyzeElementWithInline(containingFunction, false).filterIsInstance<KtNamedFunction>()
}
private fun getInlineArgumentsIfAny(inlineFunctionCalls: List<KtCallExpression>): List<KtFunction> {
return inlineFunctionCalls.flatMap {
it.valueArguments
.map(::getArgumentExpression)
.filterIsInstance<KtFunction>()
}
}
private fun getArgumentExpression(it: ValueArgument) =
(it.getArgumentExpression() as? KtLambdaExpression)?.functionLiteral ?: it.getArgumentExpression()
private fun getInlineArgumentsCallsIfAny(
sourcePosition: SourcePosition,
declarationDescriptor: DeclarationDescriptor
): List<KtCallExpression>? {
if (declarationDescriptor !is CallableDescriptor) return null
val valueParameters = declarationDescriptor.valueParameters.filter { it.type.isFunctionType }.toSet()
if (valueParameters.isEmpty()) {
return null
}
fun isCallOfArgument(ktCallExpression: KtCallExpression): Boolean {
val resolvedCall = ktCallExpression.resolveToCall() as? VariableAsFunctionResolvedCall ?: return false
val candidateDescriptor = resolvedCall.variableCall.candidateDescriptor
return candidateDescriptor in valueParameters
}
return findCallsOnPosition(sourcePosition, ::isCallOfArgument)
}
private fun getInlineFunctionCallsIfAny(sourcePosition: SourcePosition): List<KtCallExpression> {
fun isInlineCall(expr: KtCallExpression): Boolean {
val resolvedCall = expr.resolveToCall() ?: return false
return InlineUtil.isInline(resolvedCall.resultingDescriptor)
}
return findCallsOnPosition(sourcePosition, ::isInlineCall)
}
private fun findCallsOnPosition(sourcePosition: SourcePosition, filter: (KtCallExpression) -> Boolean): List<KtCallExpression> {
val file = sourcePosition.file as? KtFile ?: return emptyList()
val lineNumber = sourcePosition.line
val lineElement = findElementAtLine(file, lineNumber)
if (lineElement !is KtElement) {
if (lineElement != null) {
val call = findCallByEndToken(lineElement)
if (call != null && filter(call)) {
return listOf(call)
}
}
return emptyList()
}
val start = lineElement.startOffset
val end = lineElement.endOffset
val allFilteredCalls = CodeInsightUtils.findElementsOfClassInRange(file, start, end, KtExpression::class.java)
.map { KtPsiUtil.getParentCallIfPresent(it as KtExpression) }
.filterIsInstance<KtCallExpression>()
.filter { filter(it) }
.toSet()
// It is necessary to check range because of multiline assign
var linesRange = lineNumber..lineNumber
return allFilteredCalls.filter {
val shouldInclude = it.getLineNumber() in linesRange
if (shouldInclude) {
linesRange = min(linesRange.start, it.getLineNumber())..max(linesRange.endInclusive, it.getLineNumber(false))
}
shouldInclude
}
}
sealed class Action(
val position: XSourcePositionImpl? = null,
val stepOverInlineData: StepOverFilterData? = null
) {
class STEP_OVER : Action() {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) =
debugProcess.createStepOverCommand(suspendContext, ignoreBreakpoints).contextAction(suspendContext)
}
class STEP_OUT : Action() {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) =
debugProcess.createStepOutCommand(suspendContext).contextAction(suspendContext)
}
class RUN_TO_CURSOR(position: XSourcePositionImpl) : Action(position) {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) {
return runReadAction {
debugProcess.createRunToCursorCommand(suspendContext, position!!, ignoreBreakpoints)
}.contextAction(suspendContext)
}
}
class STEP_OVER_INLINED(stepOverInlineData: StepOverFilterData) : Action(stepOverInlineData = stepOverInlineData) {
override fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean) {
return KotlinStepActionFactory(debugProcess).createKotlinStepOverInlineAction(
KotlinStepOverInlineFilter(debugProcess.project, stepOverInlineData!!)
).contextAction(suspendContext)
}
}
abstract fun apply(debugProcess: DebugProcessImpl, suspendContext: SuspendContextImpl, ignoreBreakpoints: Boolean)
}
interface KotlinMethodFilter : MethodFilter {
fun locationMatches(context: SuspendContextImpl, location: Location): Boolean
}
fun getStepOverAction(
location: Location,
kotlinSourcePosition: KotlinSteppingCommandProvider.KotlinSourcePosition,
frameProxy: StackFrameProxyImpl,
isDexDebug: Boolean
): Action {
val inlineArgumentsToSkip = runReadAction {
getInlineCallFunctionArgumentsIfAny(kotlinSourcePosition.sourcePosition)
}
return getStepOverAction(
location, kotlinSourcePosition.file, kotlinSourcePosition.linesRange,
inlineArgumentsToSkip, frameProxy, isDexDebug
)
}
fun getStepOverAction(
location: Location,
sourceFile: KtFile,
range: IntRange,
inlineFunctionArguments: List<KtElement>,
frameProxy: StackFrameProxyImpl,
isDexDebug: Boolean
): Action {
location.declaringType() ?: return Action.STEP_OVER()
val project = sourceFile.project
val methodLocations = location.method().safeAllLineLocations()
if (methodLocations.isEmpty()) {
return Action.STEP_OVER()
}
val locationsLineAndFile = methodLocations.keysToMap { ktLocationInfo(it, isDexDebug, project, true) }
fun Location.ktLineNumber(): Int = (locationsLineAndFile[this] ?: ktLocationInfo(this, isDexDebug, project, true)).first
fun Location.ktFileName(): String {
val ktFile = (locationsLineAndFile[this] ?: ktLocationInfo(this, isDexDebug, project, true)).second
// File is not null only for inlined locations. Get file name from debugger information otherwise.
return ktFile?.name ?: this.sourceName(KOTLIN_STRATA_NAME)
}
fun isThisMethodLocation(nextLocation: Location): Boolean {
if (nextLocation.method() != location.method()) {
return false
}
val ktLineNumber = nextLocation.ktLineNumber()
if (ktLineNumber !in range) {
return false
}
try {
return nextLocation.ktFileName() == sourceFile.name
} catch (e: AbsentInformationException) {
return true
}
}
fun isBackEdgeLocation(): Boolean {
val previousSuitableLocation = methodLocations.reversed()
.dropWhile { it != location }
.drop(1)
.filter(::isThisMethodLocation)
.dropWhile { it.ktLineNumber() == location.ktLineNumber() }
.firstOrNull()
return previousSuitableLocation != null && previousSuitableLocation.ktLineNumber() > location.ktLineNumber()
}
val patchedLocation = if (isBackEdgeLocation()) {
// Pretend we had already done a backing step
methodLocations
.filter(::isThisMethodLocation)
.firstOrNull { it.ktLineNumber() == location.ktLineNumber() } ?: location
} else {
location
}
val patchedLineNumber = patchedLocation.ktLineNumber()
val lambdaArgumentRanges = runReadAction {
inlineFunctionArguments.map {
val startLineNumber = it.getLineNumber(true) + 1
val endLineNumber = it.getLineNumber(false) + 1
startLineNumber..endLineNumber
}
}
val inlineRangeVariables = getInlineRangeLocalVariables(frameProxy)
// Try to find the range for step over:
// - Lines from other files and from functions that are not in range of current one are definitely inlined and should be stepped over.
// - Lines in function arguments of inlined functions are inlined too as we found them starting from the position of inlined call.
// - Current line locations should also be stepped over.
//
// We might erroneously extend this range too much when there's a call of function argument or other
// inline function in last statement of inline function. The list of inlineRangeVariables will be used later to overcome it.
val stepOverLocations = methodLocations
.dropWhile { it != patchedLocation }
.drop(1)
.dropWhile { it.ktLineNumber() == patchedLineNumber }
.takeWhile { loc ->
!isThisMethodLocation(loc) || lambdaArgumentRanges.any { loc.ktLineNumber() in it } || loc.ktLineNumber() == patchedLineNumber
}
if (!stepOverLocations.isEmpty()) {
// Some Kotlin inlined methods with 'for' (and maybe others) generates bytecode that after dexing have a strange artifact.
// GOTO instructions are moved to the end of method and as they don't have proper line, line is obtained from the previous
// instruction. It might be method return or previous GOTO from the inlining. Simple stepping over such function is really
// terrible. On each iteration position jumps to the method end or some previous inline call and then returns back. To prevent
// this filter locations with too big code indexes manually
val returnCodeIndex: Long = if (isDexDebug) {
val method = location.method()
val locationsOfLine = method.safeLocationsOfLine(range.last)
if (locationsOfLine.isNotEmpty()) {
locationsOfLine.map { it.codeIndex() }.max() ?: -1L
} else {
findReturnFromDexBytecode(location.method())
}
} else -1L
return Action.STEP_OVER_INLINED(
StepOverFilterData(
patchedLineNumber,
stepOverLocations.map { it.ktLineNumber() }.toSet(),
inlineRangeVariables,
isDexDebug,
returnCodeIndex
)
)
}
return Action.STEP_OVER()
}
fun getStepOutAction(
location: Location,
suspendContext: SuspendContextImpl,
inlineFunctions: List<KtNamedFunction>,
inlinedArgument: KtFunctionLiteral?
): Action {
val computedReferenceType = location.declaringType() ?: return Action.STEP_OUT()
val locations = computedReferenceType.safeAllLineLocations()
val nextLineLocations = locations
.dropWhile { it != location }
.drop(1)
.filter { it.method() == location.method() }
.dropWhile { it.lineNumber() == location.lineNumber() }
if (inlineFunctions.isNotEmpty()) {
val position = suspendContext.getXPositionForStepOutFromInlineFunction(nextLineLocations, inlineFunctions)
return position?.let { Action.RUN_TO_CURSOR(it) } ?: Action.STEP_OVER()
}
if (inlinedArgument != null) {
val position = suspendContext.getXPositionForStepOutFromInlinedArgument(nextLineLocations, inlinedArgument)
return position?.let { Action.RUN_TO_CURSOR(it) } ?: Action.STEP_OVER()
}
return Action.STEP_OVER()
}
private fun SuspendContextImpl.getXPositionForStepOutFromInlineFunction(
locations: List<Location>,
inlineFunctionsToSkip: List<KtNamedFunction>
): XSourcePositionImpl? {
return getNextPositionWithFilter(locations) { offset, elementAt ->
if (inlineFunctionsToSkip.any { it.textRange.contains(offset) }) {
return@getNextPositionWithFilter true
}
getInlineArgumentIfAny(elementAt) != null
}
}
private fun SuspendContextImpl.getXPositionForStepOutFromInlinedArgument(
locations: List<Location>,
inlinedArgumentToSkip: KtFunctionLiteral
): XSourcePositionImpl? {
return getNextPositionWithFilter(locations) { offset, _ ->
inlinedArgumentToSkip.textRange.contains(offset)
}
}
private fun SuspendContextImpl.getNextPositionWithFilter(
locations: List<Location>,
skip: (Int, PsiElement) -> Boolean
): XSourcePositionImpl? {
for (location in locations) {
val position = runReadAction l@{
val sourcePosition = try {
this.debugProcess.positionManager.getSourcePosition(location)
} catch (e: NoDataException) {
null
} ?: return@l null
val file = sourcePosition.file as? KtFile ?: return@l null
val elementAt = sourcePosition.elementAt ?: return@l null
val currentLine = location.lineNumber() - 1
val lineStartOffset = file.getLineStartOffset(currentLine) ?: return@l null
if (skip(lineStartOffset, elementAt)) return@l null
XSourcePositionImpl.createByElement(elementAt)
}
if (position != null) {
return position
}
}
return null
}
fun getInlineRangeLocalVariables(stackFrame: StackFrameProxyImpl): List<LocalVariable> {
return stackFrame.safeVisibleVariables()
.filter {
val name = it.name()
name.startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION)
}
.map { it.variable }
}
private fun getInlineArgumentIfAny(elementAt: PsiElement?): KtFunctionLiteral? {
val functionLiteralExpression = elementAt?.getParentOfType<KtLambdaExpression>(false) ?: return null
val context = functionLiteralExpression.analyze(BodyResolveMode.PARTIAL)
if (!InlineUtil.isInlinedArgument(functionLiteralExpression.functionLiteral, context, false)) return null
return functionLiteralExpression.functionLiteral
}
private fun findReturnFromDexBytecode(method: Method): Long {
val methodLocations = method.safeAllLineLocations()
if (methodLocations.isEmpty()) {
return -1L
}
var lastMethodCodeIndex = methodLocations.last().codeIndex()
// Continue while it's possible to get location
while (true) {
if (method.locationOfCodeIndex(lastMethodCodeIndex + 1) != null) {
lastMethodCodeIndex++
} else {
break
}
}
var returnIndex = lastMethodCodeIndex + 1
val bytecode = method.bytecodes()
var i = bytecode.size
while (i >= 2) {
// Can step only through two-byte instructions and abort on any unknown one
i -= 2
returnIndex -= 1
val instruction = bytecode[i].toInt()
if (instruction == RETURN_VOID || instruction == RETURN || instruction == RETURN_WIDE || instruction == RETURN_OBJECT) {
// Instruction found
return returnIndex
} else if (instruction == MOVE || instruction == GOTO) {
// proceed
} else {
// Don't know the instruction and it's length. Abort.
break
}
}
return -1L
}
object DexBytecode {
val RETURN_VOID = 0x0e
val RETURN = 0x0f
val RETURN_WIDE = 0x10
val RETURN_OBJECT = 0x11
val GOTO = 0x28
val MOVE = 0x01
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingConfigurableUi">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="99d36" class="javax.swing.JCheckBox" binding="ignoreKotlinMethods">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="false"/>
<text resource-bundle="org/jetbrains/kotlin/idea/KotlinBundle" key="debugger.filter.ignore.internal.classes"/>
</properties>
</component>
<vspacer id="c37da">
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping;
import com.intellij.openapi.options.ConfigurableUi;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings;
import javax.swing.*;
public class KotlinSteppingConfigurableUi implements ConfigurableUi<KotlinDebuggerSettings> {
private JCheckBox ignoreKotlinMethods;
private JPanel myPanel;
@Override
public void reset(@NotNull KotlinDebuggerSettings settings) {
boolean flag = settings.getDEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES();
ignoreKotlinMethods.setSelected(flag);
}
@Override
public boolean isModified(@NotNull KotlinDebuggerSettings settings) {
return settings.getDEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES() != ignoreKotlinMethods.isSelected();
}
@Override
public void apply(@NotNull KotlinDebuggerSettings settings) {
settings.setDEBUG_DISABLE_KOTLIN_INTERNAL_CLASSES(ignoreKotlinMethods.isSelected());
}
@NotNull
@Override
public JComponent getComponent() {
return myPanel;
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.MethodFilter
import com.intellij.debugger.engine.RequestHint
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.psi.PsiFile
import com.intellij.util.Range
import com.intellij.xdebugger.impl.XSourcePositionImpl
import com.sun.jdi.Location
import com.sun.jdi.request.EventRequest
import org.jetbrains.kotlin.idea.debugger.isOnSuspendReturnOrReenter
import org.jetbrains.kotlin.idea.debugger.suspendFunctionFirstLineLocation
import org.jetbrains.kotlin.idea.util.application.runReadAction
class KotlinSuspendCallStepOverFilter(
private val line: Int,
private val file: PsiFile,
private val ignoreBreakpoints: Boolean
) : MethodFilter {
override fun getCallingExpressionLines(): Range<Int>? = Range(line, line)
override fun locationMatches(process: DebugProcessImpl, location: Location?): Boolean {
return location != null && isOnSuspendReturnOrReenter(location)
}
override fun onReached(context: SuspendContextImpl, hint: RequestHint): Int {
val location = context.frameProxy?.location() ?: return RequestHint.STOP
val suspendStartLineNumber = suspendFunctionFirstLineLocation(location) ?: return RequestHint.STOP
val debugProcess = context.debugProcess
val breakpointManager = DebuggerManagerEx.getInstanceEx(debugProcess.project).breakpointManager
breakpointManager.applyThreadFilter(debugProcess, null)
createRunToCursorBreakpoint(context, suspendStartLineNumber - 1, file, ignoreBreakpoints)
return RequestHint.RESUME
}
}
private fun createRunToCursorBreakpoint(context: SuspendContextImpl, line: Int, file: PsiFile, ignoreBreakpoints: Boolean) {
val position = XSourcePositionImpl.create(file.virtualFile, line) ?: return
val process = context.debugProcess
process.showStatusText(DebuggerBundle.message("status.run.to.cursor"))
process.cancelRunToCursorBreakpoint()
if (ignoreBreakpoints) {
DebuggerManagerEx.getInstanceEx(process.project).breakpointManager.disableBreakpoints(process)
}
val runToCursorBreakpoint =
runReadAction {
DebuggerManagerEx.getInstanceEx(process.project).breakpointManager.addRunToCursorBreakpoint(position, ignoreBreakpoints)
} ?: return
runToCursorBreakpoint.suspendPolicy = when {
context.suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD -> DebuggerSettings.SUSPEND_THREAD
else -> DebuggerSettings.SUSPEND_ALL
}
runToCursorBreakpoint.createRequest(process)
process.setRunToCursorBreakpoint(runToCursorBreakpoint)
}
@@ -0,0 +1,92 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.stepping
import com.intellij.debugger.engine.BreakpointStepMethodFilter
import com.intellij.debugger.engine.MethodFilter
import com.intellij.debugger.engine.RequestHint
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.openapi.diagnostic.Logger
import com.sun.jdi.VMDisconnectedException
import com.sun.jdi.request.StepRequest
import org.intellij.lang.annotations.MagicConstant
import java.lang.reflect.Field
internal class RequestHintWithMethodFilter(
stepThread: ThreadReferenceProxyImpl,
suspendContext: SuspendContextImpl,
@MagicConstant(
intValues = longArrayOf(
StepRequest.STEP_INTO.toLong(),
StepRequest.STEP_OVER.toLong(),
StepRequest.STEP_OUT.toLong()
)
) depth: Int,
methodFilter: MethodFilter
) : RequestHint(stepThread, suspendContext, methodFilter) {
private var targetMethodMatched = false
init {
// NOTE: Debugger API. Open RequestHint constructor with depth
if (depth != StepRequest.STEP_INTO) {
findFieldWithValue(StepRequest.STEP_INTO, Integer.TYPE)?.setInt(this, depth)
}
}
private fun findFieldWithValue(value: Int, type: Class<*>): Field? {
return RequestHint::class.java.declaredFields.firstOrNull { field ->
if (field.type == type) {
field.isAccessible = true
if (field.getInt(this) == value) {
return@firstOrNull true
}
}
false
}
}
override fun getNextStepDepth(context: SuspendContextImpl): Int {
try {
val frameProxy = context.frameProxy
val filter = methodFilter
if (filter != null && frameProxy != null && filter !is BreakpointStepMethodFilter) {
/*NODE: Debugger API. Base implementation works only for smart step into, and calls filter only if !isTheSameFrame(context). */
if (filter.locationMatches(context.debugProcess, frameProxy.location())) {
targetMethodMatched = true
return filter.onReached(context, this)
}
}
} catch (ignored: VMDisconnectedException) {
return STOP
} catch (e: EvaluateException) {
LOG.error(e)
return STOP
}
return super.getNextStepDepth(context)
}
override fun wasStepTargetMethodMatched(): Boolean {
return super.wasStepTargetMethodMatched() || targetMethodMatched
}
}
private val LOG = Logger.getInstance(RequestHintWithMethodFilter::class.java)
@@ -0,0 +1,21 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
compile(project(":compiler:backend"))
compile(project(":idea:jvm-debugger:eval4j"))
compile(project(":idea:idea-core"))
compile(project(":idea:idea-j2k"))
compile(project(":idea:jvm-debugger:jvm-debugger-util"))
compile(files("${System.getProperty("java.home")}/../lib/tools.jar"))
testCompile(project(":kotlin-test:kotlin-test-junit"))
testCompile(commonDep("junit:junit"))
}
sourceSets {
"main" { projectDefault() }
"test" { none() }
}
@@ -0,0 +1,14 @@
/*
* Copyright 2010-2018 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.idea.debugger.evaluate
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.completion.CompletionInformationProvider
class DebuggerFieldCompletionInformationProvider : CompletionInformationProvider {
override fun getContainerAndReceiverInformation(descriptor: DeclarationDescriptor) =
(descriptor as? DebuggerFieldPropertyDescriptor)?.description?.let { " $it" }
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2018 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.idea.debugger.evaluate
import org.jetbrains.kotlin.codegen.StackValue
import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.synthetic.JavaSyntheticPropertiesScope
class DebuggerFieldExpressionCodegenExtension : ExpressionCodegenExtension {
override fun applyProperty(receiver: StackValue, resolvedCall: ResolvedCall<*>, c: ExpressionCodegenExtension.Context): StackValue? {
val propertyDescriptor = resolvedCall.resultingDescriptor as? PropertyDescriptor ?: return null
if (propertyDescriptor is DebuggerFieldPropertyDescriptor) {
return StackValue.StackValueWithSimpleReceiver.field(
c.typeMapper.mapType(propertyDescriptor.type),
propertyDescriptor.ownerType(c.codegen.state),
propertyDescriptor.fieldName,
false,
receiver
)
}
if (propertyDescriptor is JavaPropertyDescriptor) {
val containingClass = propertyDescriptor.containingDeclaration as? JavaClassDescriptor
if (containingClass != null) {
val correspondingGetter = JavaSyntheticPropertiesScope(LockBasedStorageManager.NO_LOCKS, LookupTracker.DO_NOTHING)
.getSyntheticExtensionProperties(listOf(containingClass.defaultType), NoLookupLocation.FROM_BACKEND)
.firstOrNull { it.name == propertyDescriptor.name }
if (correspondingGetter != null) {
return c.codegen.intermediateValueForProperty(
correspondingGetter, false, false,
c.codegen.getSuperCallTarget(resolvedCall.call),
false, receiver, resolvedCall, false
)
}
}
}
return null
}
}
@@ -0,0 +1,44 @@
/*
* 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.idea.debugger.evaluate
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.idea.core.extension.KotlinIndicesHelperExtension
import org.jetbrains.kotlin.incremental.components.LookupLocation
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.synthetic.JavaSyntheticPropertiesScope
import org.jetbrains.kotlin.types.KotlinType
import java.lang.IllegalStateException
class DebuggerFieldKotlinIndicesHelperExtension : KotlinIndicesHelperExtension {
override fun appendExtensionCallables(
consumer: MutableList<in CallableDescriptor>,
moduleDescriptor: ModuleDescriptor,
receiverTypes: Collection<KotlinType>,
nameFilter: (String) -> Boolean,
lookupLocation: LookupLocation
) {
val javaPropertiesScope = JavaSyntheticPropertiesScope(LockBasedStorageManager.NO_LOCKS, LookupTracker.DO_NOTHING)
val fieldScope = DebuggerFieldSyntheticScope(javaPropertiesScope)
for (property in fieldScope.getSyntheticExtensionProperties(receiverTypes, lookupLocation)) {
if (nameFilter(property.name.asString())) {
consumer += property
}
}
}
override fun appendExtensionCallables(
consumer: MutableList<in CallableDescriptor>,
moduleDescriptor: ModuleDescriptor,
receiverTypes: Collection<KotlinType>,
nameFilter: (String) -> Boolean
) {
throw IllegalStateException("Should not be called")
}
}
@@ -0,0 +1,206 @@
/*
* Copyright 2010-2018 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.idea.debugger.evaluate
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl
import org.jetbrains.kotlin.descriptors.impl.PropertyGetterDescriptorImpl
import org.jetbrains.kotlin.idea.project.platform
import org.jetbrains.kotlin.incremental.KotlinLookupLocation
import org.jetbrains.kotlin.incremental.components.LookupLocation
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.load.java.components.JavaSourceElementFactoryImpl
import org.jetbrains.kotlin.load.java.components.TypeUsage
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor
import org.jetbrains.kotlin.load.java.lazy.types.toAttributes
import org.jetbrains.kotlin.load.java.structure.classId
import org.jetbrains.kotlin.load.kotlin.internalName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.DescriptorFactory
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
import org.jetbrains.kotlin.platform.isCommon
import org.jetbrains.kotlin.platform.jvm.isJvm
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.resolve.scopes.SyntheticScope
import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered
import org.jetbrains.kotlin.synthetic.JavaSyntheticPropertiesScope
import org.jetbrains.kotlin.synthetic.SyntheticScopeProviderExtension
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.replaceArgumentsWithStarProjections
import org.jetbrains.org.objectweb.asm.Type
class DebuggerFieldSyntheticScopeProvider : SyntheticScopeProviderExtension {
override fun getScopes(
moduleDescriptor: ModuleDescriptor,
javaSyntheticPropertiesScope: JavaSyntheticPropertiesScope
): List<SyntheticScope> {
return listOf<SyntheticScope>(DebuggerFieldSyntheticScope(javaSyntheticPropertiesScope))
}
}
class DebuggerFieldSyntheticScope(val javaSyntheticPropertiesScope: JavaSyntheticPropertiesScope) : SyntheticScope.Default() {
private val javaSourceElementFactory = JavaSourceElementFactoryImpl()
override fun getSyntheticExtensionProperties(
receiverTypes: Collection<KotlinType>,
name: Name,
location: LookupLocation
): Collection<PropertyDescriptor> {
return getSyntheticExtensionProperties(receiverTypes, location).filter { it.name == name }
}
override fun getSyntheticExtensionProperties(
receiverTypes: Collection<KotlinType>,
location: LookupLocation
): Collection<PropertyDescriptor> {
if (!isInEvaluator(location)) {
return emptyList()
}
val result = mutableListOf<PropertyDescriptor>()
for (type in receiverTypes) {
val clazz = type.constructor.declarationDescriptor as? ClassDescriptor ?: continue
result += getSyntheticPropertiesForClass(clazz)
}
return result
}
private fun isInEvaluator(location: LookupLocation): Boolean {
val element = (location as? KotlinLookupLocation)?.element ?: return false
val containingFile = element.containingFile?.takeIf { it.isValid } as? KtFile ?: return false
val platform = containingFile.platform
if (!platform.isJvm() && !platform.isCommon()) {
return false
}
return containingFile is KtCodeFragment
}
private fun getSyntheticPropertiesForClass(clazz: ClassDescriptor): Collection<PropertyDescriptor> {
val collected = mutableMapOf<Name, PropertyDescriptor>()
val syntheticPropertyNames = javaSyntheticPropertiesScope
.getSyntheticExtensionProperties(listOf(clazz.defaultType), NoLookupLocation.FROM_SYNTHETIC_SCOPE)
.mapTo(mutableSetOf()) { it.name }
collectPropertiesWithParent(clazz, syntheticPropertyNames, collected)
return collected.values
}
private tailrec fun collectPropertiesWithParent(
clazz: ClassDescriptor,
syntheticNames: Set<Name>,
consumer: MutableMap<Name, PropertyDescriptor>
) {
when (clazz) {
is LazyJavaClassDescriptor -> collectJavaProperties(clazz, syntheticNames, consumer)
is JavaClassDescriptor -> error("Unsupported Java class type")
else -> collectKotlinProperties(clazz, consumer)
}
val superClass = clazz.getSuperClassNotAny()
if (superClass != null) {
collectPropertiesWithParent(superClass, syntheticNames, consumer)
}
}
private fun collectKotlinProperties(clazz: ClassDescriptor, consumer: MutableMap<Name, PropertyDescriptor>) {
for (descriptor in clazz.unsubstitutedMemberScope.getDescriptorsFiltered(DescriptorKindFilter.VARIABLES)) {
val propertyDescriptor = descriptor as? PropertyDescriptor ?: continue
val name = propertyDescriptor.name
if (propertyDescriptor.backingField == null || name in consumer) continue
val type = propertyDescriptor.type
val sourceElement = propertyDescriptor.source
consumer[name] = createSyntheticPropertyDescriptor(clazz, type, name.asString(), "Backing field", sourceElement) { state ->
state.typeMapper.mapType(clazz.defaultType)
}
}
}
private fun collectJavaProperties(
clazz: LazyJavaClassDescriptor,
syntheticNames: Set<Name>,
consumer: MutableMap<Name, PropertyDescriptor>
) {
val javaClass = clazz.jClass
for (field in javaClass.fields) {
val fieldName = field.name
if (field.isEnumEntry || field.isStatic || fieldName in consumer || fieldName !in syntheticNames) continue
val ownerClassName = javaClass.classId?.internalName ?: continue
val typeResolver = clazz.outerContext.typeResolver
val type = typeResolver.transformJavaType(field.type, TypeUsage.COMMON.toAttributes()).replaceArgumentsWithStarProjections()
val sourceElement = javaSourceElementFactory.source(field)
consumer[fieldName] = createSyntheticPropertyDescriptor(clazz, type, fieldName.asString(), "Java field", sourceElement) {
Type.getObjectType(ownerClassName)
}
}
}
private fun createSyntheticPropertyDescriptor(
clazz: ClassDescriptor,
type: KotlinType,
fieldName: String,
description: String,
getterSource: SourceElement,
ownerType: (GenerationState) -> Type
): PropertyDescriptor {
val propertyDescriptor = DebuggerFieldPropertyDescriptor(clazz, fieldName, description, ownerType)
val extensionReceiverParameter = DescriptorFactory.createExtensionReceiverParameterForCallable(
propertyDescriptor,
clazz.defaultType.replaceArgumentsWithStarProjections(),
Annotations.EMPTY
)
propertyDescriptor.setType(type, emptyList(), null, extensionReceiverParameter)
val getter = PropertyGetterDescriptorImpl(
propertyDescriptor, Annotations.EMPTY, Modality.FINAL,
Visibilities.PUBLIC, false, false, false,
CallableMemberDescriptor.Kind.SYNTHESIZED,
null, getterSource
)
propertyDescriptor.initialize(getter, null)
return propertyDescriptor
}
}
internal class DebuggerFieldPropertyDescriptor(
containingDeclaration: DeclarationDescriptor,
val fieldName: String,
val description: String,
val ownerType: (GenerationState) -> Type
) : PropertyDescriptorImpl(
containingDeclaration,
null,
Annotations.EMPTY,
Modality.FINAL,
Visibilities.PUBLIC,
/*isVar = */true,
Name.identifier(fieldName + "_field"),
CallableMemberDescriptor.Kind.SYNTHESIZED,
SourceElement.NO_SOURCE,
/*lateInit = */false,
/*isConst = */false,
/*isExpect = */false,
/*isActual = */false,
/*isExternal = */false,
/*isDelegated = */false
)
@@ -0,0 +1,358 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiTypesUtil
import com.intellij.util.IncorrectOperationException
import com.intellij.util.concurrency.Semaphore
import com.sun.jdi.*
import org.jetbrains.annotations.TestOnly
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.util.getKotlinJvmRuntimeMarkerClass
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.DebugLabelPropertyDescriptorProvider
import org.jetbrains.kotlin.idea.debugger.getClassDescriptor
import org.jetbrains.kotlin.idea.debugger.getContextElement
import org.jetbrains.kotlin.idea.j2k.JavaToKotlinConverterFactory
import org.jetbrains.kotlin.idea.j2k.convertToKotlin
import org.jetbrains.kotlin.idea.j2k.j2k
import org.jetbrains.kotlin.idea.j2k.j2kText
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.j2k.AfterConversionPass
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.quoteIfNeeded
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.makeNullable
import java.util.concurrent.atomic.AtomicReference
class KotlinCodeFragmentFactory : CodeFragmentFactory() {
override fun createCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment {
val contextElement = getContextElement(context)
val constructor = when (item.kind) {
null -> error("Code fragment kind should be set")
CodeFragmentKind.EXPRESSION -> ::KtExpressionCodeFragment
CodeFragmentKind.CODE_BLOCK -> ::KtBlockCodeFragment
}
val codeFragment = constructor(project, "fragment.kt", item.text, initImports(item.imports), contextElement)
supplyDebugLabels(codeFragment, context)
codeFragment.putCopyableUserData(KtCodeFragment.RUNTIME_TYPE_EVALUATOR, { expression: KtExpression ->
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
val debuggerSession = debuggerContext.debuggerSession
if (debuggerSession == null || debuggerContext.suspendContext == null) {
null
} else {
val semaphore = Semaphore()
semaphore.down()
val nameRef = AtomicReference<KotlinType>()
val worker = object : KotlinRuntimeTypeEvaluator(
null, expression, debuggerContext, ProgressManager.getInstance().progressIndicator!!
) {
override fun typeCalculationFinished(type: KotlinType?) {
nameRef.set(type)
semaphore.up()
}
}
debuggerContext.debugProcess?.managerThread?.invoke(worker)
for (i in 0..50) {
ProgressManager.checkCanceled()
if (semaphore.waitFor(20)) break
}
nameRef.get()
}
})
if (contextElement != null && contextElement !is KtElement) {
codeFragment.putCopyableUserData(KtCodeFragment.FAKE_CONTEXT_FOR_JAVA_FILE, {
val emptyFile = createFakeFileWithJavaContextElement("", contextElement)
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
val debuggerSession = debuggerContext.debuggerSession
if ((debuggerSession == null || debuggerContext.suspendContext == null) && !ApplicationManager.getApplication().isUnitTestMode) {
LOG.warn("Couldn't create fake context element for java file, debugger isn't paused on breakpoint")
return@putCopyableUserData emptyFile
}
val frameDescriptor = getFrameInfo(contextElement, debuggerContext)
if (frameDescriptor == null) {
LOG.warn(
"Couldn't get info about 'this' and local variables for " +
"${debuggerContext.sourcePosition?.file?.name}:${debuggerContext.sourcePosition?.line}"
)
return@putCopyableUserData emptyFile
}
val receiverTypeReference =
frameDescriptor.thisObject?.let { createKotlinProperty(project, FAKE_JAVA_THIS_NAME, it.type().name(), it) }?.typeReference
val receiverTypeText = receiverTypeReference?.let { "${it.text}." } ?: ""
val kotlinVariablesText =
frameDescriptor.visibleVariables.entries.associate { it.key.name() to it.value }.kotlinVariablesAsText(project)
val fakeFunctionText = "fun ${receiverTypeText}$FAKE_JAVA_CONTEXT_FUNCTION_NAME() {\n$kotlinVariablesText\n}"
val fakeFile = createFakeFileWithJavaContextElement(fakeFunctionText, contextElement)
val fakeFunction = fakeFile.declarations.firstOrNull() as? KtFunction
val fakeContext = fakeFunction?.bodyBlockExpression?.statements?.lastOrNull()
return@putCopyableUserData fakeContext ?: emptyFile
})
}
return codeFragment
}
private fun supplyDebugLabels(codeFragment: KtCodeFragment, context: PsiElement?) {
val project = codeFragment.project
val debugProcess = getDebugProcess(project, context) ?: return
DebugLabelPropertyDescriptorProvider(codeFragment, debugProcess).supplyDebugLabels()
}
private fun getDebugProcess(project: Project, context: PsiElement?): DebugProcessImpl? {
return if (ApplicationManager.getApplication().isUnitTestMode) {
context?.getCopyableUserData(DEBUG_CONTEXT_FOR_TESTS)?.debugProcess
} else {
DebuggerManagerEx.getInstanceEx(project).context.debugProcess
}
}
private fun getFrameInfo(contextElement: PsiElement?, debuggerContext: DebuggerContextImpl): FrameInfo? {
val semaphore = Semaphore()
semaphore.down()
var frameInfo: FrameInfo? = null
val worker = object : DebuggerCommandImpl() {
override fun action() {
try {
val frame = if (ApplicationManager.getApplication().isUnitTestMode)
contextElement?.getCopyableUserData(DEBUG_CONTEXT_FOR_TESTS)?.frameProxy?.stackFrame
else
debuggerContext.frameProxy?.stackFrame
val visibleVariables = if (frame != null) {
val values = frame.getValues(frame.visibleVariables())
values.filterValues { it != null }
} else {
emptyMap()
}
frameInfo = FrameInfo(frame?.thisObject(), visibleVariables)
} catch (ignored: AbsentInformationException) {
// Debug info unavailable
} catch (ignored: InvalidStackFrameException) {
// Thread is resumed, the frame we have is not valid anymore
} finally {
semaphore.up()
}
}
}
debuggerContext.debugProcess?.managerThread?.invoke(worker)
for (i in 0..50) {
if (semaphore.waitFor(20)) break
}
return frameInfo
}
private class FrameInfo(val thisObject: Value?, val visibleVariables: Map<LocalVariable, Value>)
private fun initImports(imports: String?): String? {
if (imports != null && !imports.isEmpty()) {
return imports.split(KtCodeFragment.IMPORT_SEPARATOR)
.mapNotNull { fixImportIfNeeded(it) }
.joinToString(KtCodeFragment.IMPORT_SEPARATOR)
}
return null
}
private fun fixImportIfNeeded(import: String): String? {
// skip arrays
if (import.endsWith("[]")) {
return fixImportIfNeeded(import.removeSuffix("[]").trim())
}
// skip primitive types
if (PsiTypesUtil.boxIfPossible(import) != import) {
return null
}
return import
}
override fun createPresentationCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment {
val kotlinCodeFragment = createCodeFragment(item, context, project)
if (PsiTreeUtil.hasErrorElements(kotlinCodeFragment) && kotlinCodeFragment is KtExpressionCodeFragment) {
val javaExpression = try {
PsiElementFactory.SERVICE.getInstance(project).createExpressionFromText(item.text, context)
} catch (e: IncorrectOperationException) {
null
}
val importList = try {
kotlinCodeFragment.importsAsImportList()?.let {
(PsiFileFactory.getInstance(project).createFileFromText(
"dummy.java", JavaFileType.INSTANCE, it.text
) as? PsiJavaFile)?.importList
}
} catch (e: IncorrectOperationException) {
null
}
if (javaExpression != null && !PsiTreeUtil.hasErrorElements(javaExpression)) {
var convertedFragment: KtExpressionCodeFragment? = null
project.executeWriteCommand("Convert java expression to kotlin in Evaluate Expression") {
try {
val (elementResults, _, conversionContext) = javaExpression.convertToKotlin() ?: return@executeWriteCommand
val newText = elementResults.singleOrNull()?.text
val newImports = importList?.j2kText()
if (newText != null) {
convertedFragment = KtExpressionCodeFragment(
project,
kotlinCodeFragment.name,
newText,
newImports,
kotlinCodeFragment.context
)
AfterConversionPass(project, JavaToKotlinConverterFactory.createPostProcessor(formatCode = false))
.run(
convertedFragment!!,
conversionContext,
range = null
)
}
} catch (e: Throwable) {
// ignored because text can be invalid
LOG.error("Couldn't convert expression:\n`${javaExpression.text}`", e)
}
}
return convertedFragment ?: kotlinCodeFragment
}
}
return kotlinCodeFragment
}
override fun isContextAccepted(contextElement: PsiElement?): Boolean = runReadAction {
when {
// PsiCodeBlock -> DummyHolder -> originalElement
contextElement is PsiCodeBlock -> isContextAccepted(contextElement.context?.context)
contextElement == null -> false
contextElement.language == KotlinFileType.INSTANCE.language -> true
contextElement.language == JavaFileType.INSTANCE.language -> {
getKotlinJvmRuntimeMarkerClass(contextElement.project, contextElement.resolveScope) != null
}
else -> false
}
}
override fun getFileType(): KotlinFileType = KotlinFileType.INSTANCE
override fun getEvaluatorBuilder() = KotlinEvaluatorBuilder
companion object {
private val LOG = Logger.getInstance(this::class.java)
@get:TestOnly
val DEBUG_CONTEXT_FOR_TESTS: Key<DebuggerContextImpl> = Key.create("DEBUG_CONTEXT_FOR_TESTS")
const val FAKE_JAVA_CONTEXT_FUNCTION_NAME = "_java_locals_debug_fun_"
const val FAKE_JAVA_THIS_NAME = "\$this\$_java_locals_debug_fun_"
private fun Map<String, Value>.kotlinVariablesAsText(project: Project): String {
val sb = StringBuilder()
val psiNameHelper = PsiNameHelper.getInstance(project)
for ((variableName, variableValue) in entries) {
if (!psiNameHelper.isIdentifier(variableName)) continue
val variableTypeName = variableValue.type()?.name() ?: continue
val kotlinProperty = createKotlinProperty(project, variableName, variableTypeName, variableValue) ?: continue
sb.append("${kotlinProperty.text}\n")
}
sb.append("val _debug_context_val = 1\n")
return sb.toString()
}
private fun createKotlinProperty(project: Project, variableName: String, variableTypeName: String, value: Value): KtProperty? {
val actualClassDescriptor = value.asValue().asmType.getClassDescriptor(GlobalSearchScope.allScope(project))
if (actualClassDescriptor != null && actualClassDescriptor.defaultType.arguments.isEmpty()) {
val renderedType = IdeDescriptorRenderers.SOURCE_CODE.renderType(actualClassDescriptor.defaultType.makeNullable())
return KtPsiFactory(project).createProperty(variableName.quoteIfNeeded(), renderedType, false)
}
fun String.addArraySuffix() = if (value is ArrayReference) this + "[]" else this
val className = variableTypeName.replace("$", ".").substringBefore("[]")
val classType = PsiType.getTypeByName(className, project, GlobalSearchScope.allScope(project))
val type = (if (value !is PrimitiveValue && classType.resolve() == null)
CommonClassNames.JAVA_LANG_OBJECT
else
className).addArraySuffix()
val field = PsiElementFactory.SERVICE.getInstance(project)
.createField(variableName, PsiType.getTypeByName(type, project, GlobalSearchScope.allScope(project)))
val ktField = field.j2k() as? KtProperty
ktField?.modifierList?.delete()
return ktField
}
}
private fun createFakeFileWithJavaContextElement(funWithLocalVariables: String, javaContext: PsiElement): KtFile {
val javaFile = javaContext.containingFile as? PsiJavaFile
val sb = StringBuilder()
javaFile?.packageName?.takeUnless { it.isBlank() }?.let {
sb.append("package ").append(it.quoteIfNeeded()).append("\n")
}
javaFile?.importList?.let { sb.append(it.text).append("\n") }
sb.append(funWithLocalVariables)
return KtPsiFactory(javaContext.project).createAnalyzableFile("fakeFileForJavaContextInDebugger.kt", sb.toString(), javaContext)
}
}
@@ -0,0 +1,439 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.evaluation.expression.*
import com.intellij.openapi.diagnostic.Attachment
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.DumbService
import com.intellij.psi.PsiElement
import com.intellij.testFramework.runInEdtAndWait
import com.sun.jdi.*
import com.sun.jdi.Value
import org.jetbrains.eval4j.*
import org.jetbrains.eval4j.Value as Eval4JValue
import org.jetbrains.eval4j.jdi.JDIEval
import org.jetbrains.eval4j.jdi.asJdiValue
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.eval4j.jdi.makeInitialFrame
import org.jetbrains.kotlin.caches.resolve.KotlinCacheService
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.diagnostics.Severity
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
import org.jetbrains.kotlin.idea.core.util.attachmentByPsiFile
import org.jetbrains.kotlin.idea.core.util.mergeAttachments
import org.jetbrains.kotlin.idea.core.util.runInReadActionWithWriteActionPriorityWithPCE
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.Companion.compileCodeFragmentCacheAware
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.GENERATED_CLASS_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.GENERATED_FUNCTION_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.*
import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.loadClassesSafely
import org.jetbrains.kotlin.idea.debugger.evaluate.variables.EvaluatorValueConverter
import org.jetbrains.kotlin.idea.debugger.evaluate.variables.VariableFinder
import org.jetbrains.kotlin.idea.debugger.safeLocation
import org.jetbrains.kotlin.idea.debugger.safeMethod
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.AnalyzingUtils
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.isInlineClassType
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import java.util.*
internal val LOG = Logger.getInstance(KotlinEvaluator::class.java)
object KotlinEvaluatorBuilder : EvaluatorBuilder {
override fun build(codeFragment: PsiElement, position: SourcePosition?): ExpressionEvaluator {
if (codeFragment !is KtCodeFragment) {
return EvaluatorBuilderImpl.getInstance().build(codeFragment, position)
}
val context = codeFragment.context ?: evaluationException("Cannot evaluate an expression without a context")
val file = context.containingFile
if (file !is KtFile) {
reportError(codeFragment, position, "Unknown context${codeFragment.context?.javaClass}")
evaluationException("Couldn't evaluate Kotlin expression in this context")
}
return ExpressionEvaluatorImpl(KotlinEvaluator(codeFragment, position))
}
}
class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: SourcePosition?) : Evaluator {
override fun evaluate(context: EvaluationContextImpl): Any? {
if (codeFragment.text.isEmpty()) {
return context.debugProcess.virtualMachineProxy.mirrorOfVoid()
}
if (DumbService.getInstance(codeFragment.project).isDumb) {
evaluationException("Code fragment evaluation is not available in the dumb mode")
}
val frameProxy = context.frameProxy
?: evaluationException("Cannot evaluate a code fragment: frame proxy is not available")
val operatingThread = context.suspendContext.thread
?: evaluationException("Cannot evaluate a code fragment: thread is not available")
if (!operatingThread.isSuspended) {
evaluationException("Evaluation is available only for the suspended threads")
}
try {
val executionContext = ExecutionContext(context, frameProxy)
return evaluateSafe(executionContext)
} catch (e: EvaluateException) {
throw e
} catch (e: ProcessCanceledException) {
evaluationException(e)
} catch (e: Eval4JInterpretingException) {
evaluationException(e.cause)
} catch (e: Exception) {
val isSpecialException = isSpecialException(e)
if (isSpecialException) {
evaluationException(e)
}
reportError(codeFragment, sourcePosition, e.message ?: "An exception occurred", e)
val cause = if (e.message != null) ": ${e.message}" else ""
evaluationException("Cannot evaluate the expression: $cause")
}
}
private fun evaluateSafe(context: ExecutionContext): Any? {
fun compilerFactory(): CompiledDataDescriptor = compileCodeFragment(context)
val (compiledData, isCompiledDataFromCache) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory)
val classLoaderRef = loadClassesSafely(context, compiledData.classes)
val result = if (classLoaderRef != null) {
evaluateWithCompilation(context, compiledData, classLoaderRef)
?: evaluateWithEval4J(context, compiledData, classLoaderRef)
} else {
evaluateWithEval4J(context, compiledData, classLoaderRef)
}
// If bytecode was taken from cache and exception was thrown - recompile bytecode and run eval4j again
if (isCompiledDataFromCache && result is ExceptionThrown && result.kind == ExceptionThrown.ExceptionKind.BROKEN_CODE) {
val (recompiledData, _) = compileCodeFragmentCacheAware(codeFragment, sourcePosition, ::compilerFactory, force = true)
return evaluateWithEval4J(context, recompiledData, classLoaderRef).toJdiValue(context)
}
return when (result) {
is InterpreterResult -> result.toJdiValue(context)
else -> result
}
}
private fun compileCodeFragment(context: ExecutionContext): CompiledDataDescriptor {
val debugProcess = context.debugProcess
var analysisResult = checkForErrors(codeFragment, debugProcess)
if (codeFragment.wrapToStringIfNeeded(analysisResult.bindingContext)) {
// Repeat analysis with toString() added
analysisResult = checkForErrors(codeFragment, debugProcess)
}
val (bindingContext) = runReadAction {
DebuggerUtils.analyzeInlinedFunctions(
KotlinCacheService.getInstance(codeFragment.project).getResolutionFacade(listOf(codeFragment)),
codeFragment, false, analysisResult.bindingContext
)
}
val moduleDescriptor = analysisResult.moduleDescriptor
val result = CodeFragmentCompiler(context).compile(codeFragment, bindingContext, moduleDescriptor)
return createCompiledDataDescriptor(result, sourcePosition)
}
private fun KtCodeFragment.wrapToStringIfNeeded(bindingContext: BindingContext): Boolean {
if (this !is KtExpressionCodeFragment) {
return false
}
val contentElement = runReadAction { getContentElement() }
val expressionType = bindingContext[BindingContext.EXPRESSION_TYPE_INFO, contentElement]?.type
if (contentElement != null && expressionType?.isInlineClassType() == true) {
val newExpression = runReadAction {
val expressionText = contentElement.text
KtPsiFactory(project).createExpression("($expressionText).toString()")
}
runInEdtAndWait {
project.executeWriteCommand("Wrap with 'toString()'") {
contentElement.replace(newExpression)
}
}
return true
}
return false
}
private data class ErrorCheckingResult(
val bindingContext: BindingContext,
val moduleDescriptor: ModuleDescriptor,
val files: List<KtFile>
)
private fun checkForErrors(codeFragment: KtCodeFragment, debugProcess: DebugProcessImpl): ErrorCheckingResult {
return runInReadActionWithWriteActionPriorityWithPCE {
try {
AnalyzingUtils.checkForSyntacticErrors(codeFragment)
} catch (e: IllegalArgumentException) {
evaluationException(e.message ?: e.toString())
}
val filesToAnalyze = listOf(codeFragment)
val resolutionFacade = KotlinCacheService.getInstance(codeFragment.project).getResolutionFacade(filesToAnalyze)
DebugLabelPropertyDescriptorProvider(codeFragment, debugProcess).supplyDebugLabels()
val analysisResult = resolutionFacade.analyzeWithAllCompilerChecks(filesToAnalyze)
if (analysisResult.isError()) {
evaluationException(analysisResult.error)
}
val bindingContext = analysisResult.bindingContext
bindingContext.diagnostics
.filter { it.factory !in IGNORED_DIAGNOSTICS }
.firstOrNull { it.severity == Severity.ERROR && it.psiElement.containingFile == codeFragment }
?.let { evaluationException(DefaultErrorMessages.render(it)) }
ErrorCheckingResult(bindingContext, analysisResult.moduleDescriptor, Collections.singletonList(codeFragment))
}
}
private fun evaluateWithCompilation(
context: ExecutionContext,
compiledData: CompiledDataDescriptor,
classLoader: ClassLoaderReference
): Value? {
return try {
runEvaluation(context, compiledData, classLoader) { args ->
val mainClassType = context.findClass(GENERATED_CLASS_NAME, classLoader) as? ClassType
?: error("Can not find class \"$GENERATED_CLASS_NAME\"")
val mainMethod = mainClassType.methods().single { it.name() == GENERATED_FUNCTION_NAME }
val returnValue = context.invokeMethod(mainClassType, mainMethod, args)
EvaluatorValueConverter(context).unref(returnValue)
}
} catch (e: Throwable) {
LOG.error("Unable to evaluate the expression with compilation", e)
return null
}
}
private fun evaluateWithEval4J(
context: ExecutionContext,
compiledData: CompiledDataDescriptor,
classLoader: ClassLoaderReference?
): InterpreterResult {
val mainClassBytecode = compiledData.mainClass.bytes
val mainClassAsmNode = ClassNode().apply { ClassReader(mainClassBytecode).accept(this, 0) }
val mainMethod = mainClassAsmNode.methods.first { it.name == GENERATED_FUNCTION_NAME }
return runEvaluation(context, compiledData, classLoader ?: context.evaluationContext.classLoader) { args ->
val vm = context.vm.virtualMachine
val thread = context.suspendContext.thread?.threadReference?.takeIf { it.isSuspended }
?: error("Can not find a thread to run evaluation on")
val eval = JDIEval(vm, classLoader, thread, context.invokePolicy)
interpreterLoop(mainMethod, makeInitialFrame(mainMethod, args.map { it.asValue() }), eval)
}
}
private fun <T> runEvaluation(
context: ExecutionContext,
compiledData: CompiledDataDescriptor,
classLoader: ClassLoaderReference?,
block: (List<Value?>) -> T
): T {
// Preload additional classes
compiledData.classes
.filter { !it.isMainClass }
.forEach { context.findClass(it.className, classLoader) }
return context.vm.virtualMachine.executeWithBreakpointsDisabled {
for (parameterType in compiledData.mainMethodSignature.parameterTypes) {
context.findClass(parameterType, classLoader)
}
val args = calculateMainMethodCallArguments(context, compiledData)
block(args)
}
}
private fun calculateMainMethodCallArguments(context: ExecutionContext, compiledData: CompiledDataDescriptor): List<Value?> {
val asmValueParameters = compiledData.mainMethodSignature.parameterTypes
val valueParameters = compiledData.parameters
require(asmValueParameters.size == valueParameters.size)
val args = valueParameters.zip(asmValueParameters)
val variableFinder = VariableFinder(context)
return args.map { (parameter, asmType) ->
val result = variableFinder.find(parameter, asmType)
if (result == null) {
val name = parameter.debugString
fun isInsideDefaultInterfaceMethod(): Boolean {
val method = context.frameProxy.safeLocation()?.safeMethod() ?: return false
val desc = method.signature()
return method.name().endsWith("\$default") && DEFAULT_METHOD_MARKERS.any { desc.contains("I${it.descriptor})") }
}
if (parameter in compiledData.crossingBounds) {
evaluationException("'$name' is not captured")
} else if (parameter.kind == CodeFragmentParameter.Kind.FIELD_VAR) {
evaluationException("Cannot find the backing field '${parameter.name}'")
} else if (parameter.kind == CodeFragmentParameter.Kind.ORDINARY && isInsideDefaultInterfaceMethod()) {
evaluationException("Parameter evaluation is not supported for '\$default' methods")
} else {
throw VariableFinder.variableNotFound(context, buildString {
append("Cannot find local variable: name = '").append(name).append("', type = ").append(asmType.className)
})
}
}
result.value
}
}
override fun getModifier() = null
companion object {
private val IGNORED_DIAGNOSTICS: Set<DiagnosticFactory<*>> =
Errors.INVISIBLE_REFERENCE_DIAGNOSTICS + setOf(Errors.EXPERIMENTAL_API_USAGE_ERROR)
private val DEFAULT_METHOD_MARKERS = listOf(AsmTypes.OBJECT_TYPE, AsmTypes.DEFAULT_CONSTRUCTOR_MARKER)
private fun InterpreterResult.toJdiValue(context: ExecutionContext): Value? {
val jdiValue = when (this) {
is ValueReturned -> result
is ExceptionThrown -> {
when {
this.kind == ExceptionThrown.ExceptionKind.FROM_EVALUATED_CODE ->
evaluationException(InvocationException(this.exception.value as ObjectReference))
this.kind == ExceptionThrown.ExceptionKind.BROKEN_CODE ->
throw exception.value as Throwable
else ->
evaluationException(exception.toString())
}
}
is AbnormalTermination -> evaluationException(message)
else -> throw IllegalStateException("Unknown result value produced by eval4j")
}
val sharedVar = if ((jdiValue is AbstractValue<*>)) getValueIfSharedVar(jdiValue, context) else null
return sharedVar?.value ?: jdiValue.asJdiValue(context.vm.virtualMachine, jdiValue.asmType)
}
private fun getValueIfSharedVar(value: Eval4JValue, context: ExecutionContext): VariableFinder.Result? {
val obj = value.obj(value.asmType) as? ObjectReference ?: return null
return VariableFinder.Result(EvaluatorValueConverter(context).unref(obj))
}
}
}
private fun <T> VirtualMachine.executeWithBreakpointsDisabled(block: () -> T): T {
val allRequests = eventRequestManager().breakpointRequests() + eventRequestManager().classPrepareRequests()
try {
allRequests.forEach { it.disable() }
return block()
} finally {
allRequests.forEach { it.enable() }
}
}
private fun isSpecialException(th: Throwable): Boolean {
return when (th) {
is ClassNotPreparedException,
is InternalException,
is AbsentInformationException,
is ClassNotLoadedException,
is IncompatibleThreadStateException,
is InconsistentDebugInfoException,
is ObjectCollectedException,
is VMDisconnectedException -> true
else -> false
}
}
private fun reportError(codeFragment: KtCodeFragment, position: SourcePosition?, message: String, throwable: Throwable? = null) {
runReadAction {
val contextFile = codeFragment.context?.containingFile
val attachments = arrayOf(
attachmentByPsiFile(contextFile),
attachmentByPsiFile(codeFragment),
Attachment("breakpoint.info", "Position: " + position?.run { "${file.name}:$line" }),
Attachment("context.info", runReadAction { codeFragment.context?.text ?: "null" })
)
LOG.error(
"Cannot evaluate a code fragment of type " + codeFragment::class.java + ": " + message.decapitalize(),
throwable,
mergeAttachments(*attachments)
)
}
}
fun createCompiledDataDescriptor(result: CodeFragmentCompiler.CompilationResult, sourcePosition: SourcePosition?): CompiledDataDescriptor {
val localFunctionSuffixes = result.localFunctionSuffixes
val dumbParameters = ArrayList<CodeFragmentParameter.Dumb>(result.parameterInfo.parameters.size)
for (parameter in result.parameterInfo.parameters) {
val dumb = parameter.dumb
if (dumb.kind == CodeFragmentParameter.Kind.LOCAL_FUNCTION) {
val suffix = localFunctionSuffixes[dumb]
if (suffix != null) {
dumbParameters += dumb.copy(name = dumb.name + suffix)
continue
}
}
dumbParameters += dumb
}
return CompiledDataDescriptor(
result.classes,
dumbParameters,
result.parameterInfo.crossingBounds,
result.mainMethodSignature,
sourcePosition
)
}
private fun evaluationException(msg: String): Nothing = throw EvaluateExceptionUtil.createEvaluateException(msg)
private fun evaluationException(e: Throwable): Nothing = throw EvaluateExceptionUtil.createEvaluateException(e)
@@ -0,0 +1,110 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.ContextUtil
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.evaluation.expression.ExpressionEvaluator
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.ui.EditorEvaluationCommand
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.CommonClassNames
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.ClassType
import com.sun.jdi.Value
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.idea.debugger.getClassDescriptor
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.org.objectweb.asm.Type as AsmType
abstract class KotlinRuntimeTypeEvaluator(
editor: Editor?,
expression: KtExpression,
context: DebuggerContextImpl,
indicator: ProgressIndicator
) : EditorEvaluationCommand<KotlinType>(editor, expression, context, indicator) {
override fun threadAction() {
var type: KotlinType? = null
try {
type = evaluate()
} catch (ignored: ProcessCanceledException) {
} catch (ignored: EvaluateException) {
} finally {
typeCalculationFinished(type)
}
}
protected abstract fun typeCalculationFinished(type: KotlinType?)
override fun evaluate(evaluationContext: EvaluationContextImpl): KotlinType? {
val project = evaluationContext.project
val evaluator = DebuggerInvocationUtil.commitAndRunReadAction<ExpressionEvaluator>(project) {
val codeFragment = KtPsiFactory(myElement.project).createExpressionCodeFragment(
myElement.text, myElement.containingFile.context
)
KotlinEvaluatorBuilder.build(codeFragment, ContextUtil.getSourcePosition(evaluationContext))
}
val value = evaluator.evaluate(evaluationContext)
if (value != null) {
return runReadAction { getCastableRuntimeType(evaluationContext.debugProcess.searchScope, value) }
}
throw EvaluateExceptionUtil.createEvaluateException(DebuggerBundle.message("evaluation.error.surrounded.expression.null"))
}
companion object {
private fun getCastableRuntimeType(scope: GlobalSearchScope, value: Value): KotlinType? {
val myValue = value.asValue()
var psiClass = myValue.asmType.getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
val type = value.type()
if (type is ClassType) {
val superclass = type.superclass()
if (superclass != null && CommonClassNames.JAVA_LANG_OBJECT != superclass.name()) {
psiClass = AsmType.getType(superclass.signature()).getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
}
for (interfaceType in type.interfaces()) {
psiClass = AsmType.getType(interfaceType.signature()).getClassDescriptor(scope)
if (psiClass != null) {
return psiClass.defaultType
}
}
}
return null
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.classLoading
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
abstract class AbstractAndroidClassLoadingAdapter : ClassLoadingAdapter {
protected fun dex(context: ExecutionContext, classes: Collection<ClassToLoad>): ByteArray? {
return AndroidDexer.getInstances(context.project).single().dex(classes)
}
protected fun wrapToByteBuffer(bytes: ArrayReference, context: ExecutionContext): ObjectReference {
val classLoader = context.classLoader
val byteBufferClass = context.findClass("java.nio.ByteBuffer", classLoader) as ClassType
val wrapMethod = byteBufferClass.concreteMethodByName("wrap", "([B)Ljava/nio/ByteBuffer;")
?: error("'wrap' method not found")
return context.invokeMethod(byteBufferClass, wrapMethod, listOf(bytes)) as ObjectReference
}
protected fun tryLoadClass(context: ExecutionContext, fqName: String, classLoader: ClassLoaderReference?): ReferenceType? {
return try {
context.debugProcess.loadClass(context.evaluationContext, fqName, classLoader)
} catch (e: Throwable) {
null
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.classLoading
import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor
interface AndroidDexer {
companion object : ProjectExtensionDescriptor<AndroidDexer>(
"org.jetbrains.kotlin.androidDexer", AndroidDexer::class.java
)
fun dex(classes: Collection<ClassToLoad>): ByteArray?
}
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.classLoading
import com.intellij.debugger.engine.JVMNameUtil
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.isDexDebug
class AndroidOClassLoadingAdapter : AbstractAndroidClassLoadingAdapter() {
override fun isApplicable(context: ExecutionContext, info: ClassLoadingAdapter.Companion.ClassInfoForEvaluator) = with(info) {
isCompilingEvaluatorPreferred && context.debugProcess.isDexDebug()
}
private fun resolveClassLoaderClass(context: ExecutionContext): ClassType? {
return try {
val classLoader = context.classLoader
tryLoadClass(context, "dalvik.system.InMemoryDexClassLoader", classLoader) as? ClassType
} catch (e: EvaluateException) {
null
}
}
override fun loadClasses(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference {
val inMemoryClassLoaderClass = resolveClassLoaderClass(context) ?: error("InMemoryDexClassLoader class not found")
val constructorMethod = inMemoryClassLoaderClass.concreteMethodByName(
JVMNameUtil.CONSTRUCTOR_NAME, "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"
) ?: error("Constructor method not found")
val dexBytes = dex(context, classes) ?: error("Can't dex classes")
val dexBytesMirror = mirrorOfByteArray(dexBytes, context)
val dexByteBuffer = wrapToByteBuffer(dexBytesMirror, context)
val classLoader = context.classLoader
val args = listOf(dexByteBuffer, classLoader)
val newClassLoader = context.newInstance(inMemoryClassLoaderClass, constructorMethod, args) as ClassLoaderReference
context.keepReference(newClassLoader)
return newClassLoader
}
}
@@ -0,0 +1,116 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.classLoading
import com.sun.jdi.ArrayReference
import com.sun.jdi.ArrayType
import com.sun.jdi.ClassLoaderReference
import com.sun.jdi.Value
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.Label
import org.jetbrains.org.objectweb.asm.tree.*
import kotlin.math.min
interface ClassLoadingAdapter {
companion object {
private const val CHUNK_SIZE = 4096
private val ADAPTERS = listOf(
AndroidOClassLoadingAdapter(),
OrdinaryClassLoadingAdapter()
)
fun loadClasses(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference? {
val mainClass = classes.firstOrNull { it.isMainClass } ?: return null
var info = ClassInfoForEvaluator(containsAdditionalClasses = classes.size > 1)
if (!info.containsAdditionalClasses) {
info = analyzeClass(mainClass, info)
}
for (adapter in ADAPTERS) {
if (adapter.isApplicable(context, info)) {
return adapter.loadClasses(context, classes)
}
}
return null
}
data class ClassInfoForEvaluator(
val containsLoops: Boolean = false,
val containsCodeUnsupportedInEval4J: Boolean = false,
val containsAdditionalClasses: Boolean = false
) {
val isCompilingEvaluatorPreferred: Boolean
get() = containsLoops || containsCodeUnsupportedInEval4J || containsAdditionalClasses
}
private fun analyzeClass(classToLoad: ClassToLoad, info: ClassInfoForEvaluator): ClassInfoForEvaluator {
val classNode = ClassNode().apply { ClassReader(classToLoad.bytes).accept(this, 0) }
val methodToRun = classNode.methods.single { it.name == GENERATED_FUNCTION_NAME }
val visitedLabels = hashSetOf<Label>()
tailrec fun analyzeInsn(insn: AbstractInsnNode, info: ClassInfoForEvaluator): ClassInfoForEvaluator {
when (insn) {
is LabelNode -> visitedLabels += insn.label
is JumpInsnNode -> {
if (insn.label.label in visitedLabels) {
return info.copy(containsLoops = true)
}
}
is TableSwitchInsnNode, is LookupSwitchInsnNode -> {
return info.copy(containsCodeUnsupportedInEval4J = true)
}
}
val nextInsn = insn.next ?: return info
return analyzeInsn(nextInsn, info)
}
val firstInsn = methodToRun.instructions?.first ?: return info
return analyzeInsn(firstInsn, info)
}
}
fun isApplicable(context: ExecutionContext, info: ClassInfoForEvaluator): Boolean
fun loadClasses(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference
fun mirrorOfByteArray(bytes: ByteArray, context: ExecutionContext): ArrayReference {
val classLoader = context.classLoader
val arrayClass = context.findClass("byte[]", classLoader) as ArrayType
val reference = context.newInstance(arrayClass, bytes.size)
context.keepReference(reference)
val mirrors = ArrayList<Value>(bytes.size)
for (byte in bytes) {
mirrors += context.vm.mirrorOf(byte)
}
var loaded = 0
while (loaded < mirrors.size) {
val chunkSize = min(CHUNK_SIZE, mirrors.size - loaded)
reference.setValues(loaded, mirrors, loaded, chunkSize)
loaded += chunkSize
}
return reference
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.classLoading
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.impl.ClassLoadingUtils
import com.intellij.openapi.projectRoots.JavaSdkVersion
import com.sun.jdi.ClassLoaderReference
import com.sun.jdi.ClassType
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.isDexDebug
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.ClassWriter
import org.jetbrains.org.objectweb.asm.Opcodes
class OrdinaryClassLoadingAdapter : ClassLoadingAdapter {
private companion object {
// This list should contain all superclasses of lambda classes.
// The order is relevant here: if we load Lambda first instead, during the definition of Lambda the class loader will try
// to load its superclass. It will succeed, probably with the help of some parent class loader, and the subsequent attempt to define
// the patched version of that superclass will fail with LinkageError (cannot redefine class)
private val LAMBDA_SUPERCLASSES = listOf(ClassBytes("kotlin.jvm.internal.Lambda"))
// Copied from com.intellij.debugger.ui.impl.watch.CompilingEvaluator.changeSuperToMagicAccessor
fun changeSuperToMagicAccessor(bytes: ByteArray): ByteArray {
val classWriter = ClassWriter(0)
val classVisitor = object : ClassVisitor(Opcodes.API_VERSION, classWriter) {
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array<String>?
) {
var newSuperName = superName
if ("java/lang/Object" == newSuperName) {
newSuperName = "sun/reflect/MagicAccessorImpl"
}
super.visit(version, access, name, signature, newSuperName, interfaces)
}
}
ClassReader(bytes).accept(classVisitor, 0)
return classWriter.toByteArray()
}
fun useMagicAccessor(context: ExecutionContext): Boolean {
val rawVersion = context.vm.version()?.substringBefore('_') ?: return false
val javaVersion = JavaSdkVersion.fromVersionString(rawVersion) ?: return false
return !javaVersion.isAtLeast(JavaSdkVersion.JDK_1_9)
}
}
override fun isApplicable(context: ExecutionContext, info: ClassLoadingAdapter.Companion.ClassInfoForEvaluator): Boolean {
return info.isCompilingEvaluatorPreferred && context.classLoader != null && !context.debugProcess.isDexDebug()
}
override fun loadClasses(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference {
val process = context.debugProcess
val classLoader = try {
ClassLoadingUtils.getClassLoader(context.evaluationContext, process)
} catch (e: Exception) {
throw EvaluateException("Error creating evaluation class loader: $e", e)
}
try {
defineClasses(classes, context, classLoader)
} catch (e: Exception) {
throw EvaluateException("Error during classes definition $e", e)
}
return classLoader
}
private fun defineClasses(
classes: Collection<ClassToLoad>,
context: ExecutionContext,
classLoader: ClassLoaderReference
) {
val classesToLoad = if (classes.size == 1) {
// No need in loading lambda superclass if there're no lambdas
classes
} else {
val lambdaSuperclasses = LAMBDA_SUPERCLASSES.map {
ClassToLoad(it.name, it.name.replace('.', '/') + ".class", it.bytes)
}
lambdaSuperclasses + classes
}
for ((className, _, bytes) in classesToLoad) {
val patchedBytes = if (useMagicAccessor(context)) changeSuperToMagicAccessor(bytes) else bytes
defineClass(className, patchedBytes, context, classLoader)
}
}
private fun defineClass(
name: String,
bytes: ByteArray,
context: ExecutionContext,
classLoader: ClassLoaderReference
) {
try {
val vm = context.vm
val classLoaderType = classLoader.referenceType() as ClassType
val defineMethod = classLoaderType.concreteMethodByName("defineClass", "(Ljava/lang/String;[BII)Ljava/lang/Class;")
val nameObj = vm.mirrorOf(name)
val args = listOf(nameObj, mirrorOfByteArray(bytes, context), vm.mirrorOf(0), vm.mirrorOf(bytes.size))
context.invokeMethod(classLoader, defineMethod, args)
} catch (e: Exception) {
throw EvaluateException("Error during class $name definition: $e", e)
}
}
private class ClassBytes(val name: String) {
val bytes: ByteArray by lazy {
val inputStream = this::class.java.classLoader.getResourceAsStream(name.replace('.', '/') + ".class")
?: throw EvaluateException("Couldn't find $name class in current class loader")
inputStream.use {
it.readBytes()
}
}
}
}
@@ -0,0 +1,287 @@
/*
* 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.idea.debugger.evaluate.compilation
import org.jetbrains.kotlin.backend.common.output.OutputFile
import org.jetbrains.kotlin.caches.resolve.KotlinCacheService
import org.jetbrains.kotlin.codegen.*
import org.jetbrains.kotlin.codegen.CodeFragmentCodegen.Companion.getSharedTypeIfApplicable
import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension.Context as InCo
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.impl.*
import org.jetbrains.kotlin.idea.debugger.evaluate.*
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.GENERATED_CLASS_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.GENERATED_FUNCTION_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.CompiledDataDescriptor.MethodSignature
import org.jetbrains.kotlin.idea.project.languageVersionSettings
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.incremental.components.LookupLocation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.ResolveSession
import org.jetbrains.kotlin.resolve.lazy.data.KtClassOrObjectInfo
import org.jetbrains.kotlin.resolve.lazy.data.KtScriptInfo
import org.jetbrains.kotlin.resolve.lazy.declarations.PackageMemberDeclarationProvider
import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyPackageDescriptor
import org.jetbrains.kotlin.resolve.scopes.ChainedMemberScope
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.resolve.scopes.MemberScope
import org.jetbrains.kotlin.resolve.scopes.MemberScopeImpl
import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.SimpleType
import org.jetbrains.kotlin.utils.Printer
import org.jetbrains.org.objectweb.asm.Type
class CodeFragmentCompiler(private val executionContext: ExecutionContext) {
data class CompilationResult(
val classes: List<ClassToLoad>,
val parameterInfo: CodeFragmentParameterInfo,
val localFunctionSuffixes: Map<CodeFragmentParameter.Dumb, String>,
val mainMethodSignature: MethodSignature
)
fun compile(codeFragment: KtCodeFragment, bindingContext: BindingContext, moduleDescriptor: ModuleDescriptor): CompilationResult {
return runReadAction { doCompile(codeFragment, bindingContext, moduleDescriptor) }
}
private fun doCompile(
codeFragment: KtCodeFragment, bindingContext: BindingContext, moduleDescriptor: ModuleDescriptor
): CompilationResult {
require(codeFragment is KtBlockCodeFragment || codeFragment is KtExpressionCodeFragment) {
"Unsupported code fragment type: $codeFragment"
}
val project = codeFragment.project
val resolutionFacade = KotlinCacheService.getInstance(project).getResolutionFacade(listOf(codeFragment))
val resolveSession = resolutionFacade.getFrontendService(ResolveSession::class.java)
val moduleDescriptorWrapper = EvaluatorModuleDescriptor(codeFragment, moduleDescriptor, resolveSession)
val defaultReturnType = moduleDescriptor.builtIns.unitType
val returnType = getReturnType(codeFragment, bindingContext, defaultReturnType)
val compilerConfiguration = CompilerConfiguration()
compilerConfiguration.languageVersionSettings = codeFragment.languageVersionSettings
val generationState = GenerationState.Builder(
project, ClassBuilderFactories.BINARIES, moduleDescriptorWrapper,
bindingContext, listOf(codeFragment), compilerConfiguration
).build()
val parameterInfo = CodeFragmentParameterAnalyzer(executionContext, codeFragment, bindingContext).analyze()
val (classDescriptor, methodDescriptor) = createDescriptorsForCodeFragment(
codeFragment, Name.identifier(GENERATED_CLASS_NAME), Name.identifier(GENERATED_FUNCTION_NAME),
parameterInfo, returnType, moduleDescriptorWrapper.packageFragmentForEvaluator
)
val codegenInfo = CodeFragmentCodegenInfo(classDescriptor, methodDescriptor, parameterInfo.parameters)
CodeFragmentCodegen.setCodeFragmentInfo(codeFragment, codegenInfo)
KotlinCodegenFacade.compileCorrectFiles(generationState, CompilationErrorHandler.THROW_EXCEPTION)
val classes = generationState.factory.asList().filterClassFiles()
.map { ClassToLoad(it.internalClassName, it.relativePath, it.asByteArray()) }
val methodSignature = getMethodSignature(methodDescriptor, parameterInfo.parameters, generationState)
val functionSuffixes = getLocalFunctionSuffixes(parameterInfo.parameters, generationState.typeMapper)
generationState.destroy()
return CompilationResult(classes, parameterInfo, functionSuffixes, methodSignature)
}
private fun getLocalFunctionSuffixes(
parameters: List<CodeFragmentParameter.Smart>,
typeMapper: KotlinTypeMapper
): Map<CodeFragmentParameter.Dumb, String> {
val result = mutableMapOf<CodeFragmentParameter.Dumb, String>()
for (parameter in parameters) {
if (parameter.kind != CodeFragmentParameter.Kind.LOCAL_FUNCTION) {
continue
}
val ownerClassName = typeMapper.mapOwner(parameter.targetDescriptor).internalName
val lastDollarIndex = ownerClassName.lastIndexOf('$').takeIf { it >= 0 } ?: continue
result[parameter.dumb] = ownerClassName.drop(lastDollarIndex)
}
return result
}
private fun getMethodSignature(
methodDescriptor: FunctionDescriptor,
parameters: List<CodeFragmentParameter.Smart>,
state: GenerationState
): MethodSignature {
val typeMapper = state.typeMapper
val asmSignature = typeMapper.mapSignatureSkipGeneric(methodDescriptor)
val asmParameters = parameters.zip(asmSignature.valueParameters).map { (param, sigParam) ->
getSharedTypeIfApplicable(param.targetDescriptor, typeMapper) ?: sigParam.asmType
}
return MethodSignature(asmParameters, asmSignature.returnType)
}
private fun getReturnType(
codeFragment: KtCodeFragment,
bindingContext: BindingContext,
defaultReturnType: SimpleType
): KotlinType {
return when (codeFragment) {
is KtExpressionCodeFragment -> {
val typeInfo = bindingContext[BindingContext.EXPRESSION_TYPE_INFO, codeFragment.getContentElement()]
typeInfo?.type ?: defaultReturnType
}
is KtBlockCodeFragment -> {
val blockExpression = codeFragment.getContentElement()
val lastStatement = blockExpression.statements.lastOrNull() ?: return defaultReturnType
val typeInfo = bindingContext[BindingContext.EXPRESSION_TYPE_INFO, lastStatement]
typeInfo?.type ?: defaultReturnType
}
else -> defaultReturnType
}
}
private fun createDescriptorsForCodeFragment(
declaration: KtCodeFragment,
className: Name,
methodName: Name,
parameterInfo: CodeFragmentParameterInfo,
returnType: KotlinType,
packageFragmentDescriptor: PackageFragmentDescriptor
): Pair<ClassDescriptor, FunctionDescriptor> {
val classDescriptor = ClassDescriptorImpl(
packageFragmentDescriptor, className, Modality.FINAL, ClassKind.OBJECT,
emptyList(),
KotlinSourceElement(declaration),
false,
LockBasedStorageManager.NO_LOCKS
)
val methodDescriptor = SimpleFunctionDescriptorImpl.create(
classDescriptor, Annotations.EMPTY, methodName,
CallableMemberDescriptor.Kind.SYNTHESIZED, classDescriptor.source
)
val parameters = parameterInfo.parameters.mapIndexed { index, parameter ->
ValueParameterDescriptorImpl(
methodDescriptor, null, index, Annotations.EMPTY, Name.identifier("p$index"),
parameter.targetType,
declaresDefaultValue = false,
isCrossinline = false,
isNoinline = false,
varargElementType = null,
source = SourceElement.NO_SOURCE
)
}
methodDescriptor.initialize(
null, classDescriptor.thisAsReceiverParameter, emptyList(),
parameters, returnType, Modality.FINAL, Visibilities.PUBLIC
)
val memberScope = EvaluatorMemberScopeForMethod(methodDescriptor)
val constructor = ClassConstructorDescriptorImpl.create(classDescriptor, Annotations.EMPTY, true, classDescriptor.source)
classDescriptor.initialize(memberScope, setOf(constructor), constructor)
return Pair(classDescriptor, methodDescriptor)
}
}
private class EvaluatorMemberScopeForMethod(private val methodDescriptor: SimpleFunctionDescriptor) : MemberScopeImpl() {
override fun getContributedFunctions(name: Name, location: LookupLocation): Collection<SimpleFunctionDescriptor> {
return if (name == methodDescriptor.name) {
listOf(methodDescriptor)
} else {
emptyList()
}
}
override fun getContributedDescriptors(
kindFilter: DescriptorKindFilter,
nameFilter: (Name) -> Boolean
): Collection<DeclarationDescriptor> {
return if (kindFilter.accepts(methodDescriptor) && nameFilter(methodDescriptor.name)) {
listOf(methodDescriptor)
} else {
emptyList()
}
}
override fun getFunctionNames() = setOf(methodDescriptor.name)
override fun printScopeStructure(p: Printer) {
p.println(this::class.java.simpleName)
}
}
private class EvaluatorModuleDescriptor(
val codeFragment: KtCodeFragment,
val moduleDescriptor: ModuleDescriptor,
resolveSession: ResolveSession
) : ModuleDescriptor by moduleDescriptor {
private val declarationProvider = object : PackageMemberDeclarationProvider {
override fun getPackageFiles() = listOf(codeFragment)
override fun containsFile(file: KtFile) = file == codeFragment
override fun getDeclarationNames() = emptySet<Name>()
override fun getDeclarations(kindFilter: DescriptorKindFilter, nameFilter: (Name) -> Boolean) = emptyList<KtDeclaration>()
override fun getClassOrObjectDeclarations(name: Name) = emptyList<KtClassOrObjectInfo<*>>()
override fun getAllDeclaredSubPackages(nameFilter: (Name) -> Boolean) = emptyList<FqName>()
override fun getFunctionDeclarations(name: Name) = emptyList<KtNamedFunction>()
override fun getPropertyDeclarations(name: Name) = emptyList<KtProperty>()
override fun getTypeAliasDeclarations(name: Name) = emptyList<KtTypeAlias>()
override fun getDestructuringDeclarationsEntries(name: Name) = emptyList<KtDestructuringDeclarationEntry>()
override fun getScriptDeclarations(name: Name) = emptyList<KtScriptInfo>()
}
val packageFragmentForEvaluator = LazyPackageDescriptor(this, FqName.ROOT, resolveSession, declarationProvider)
override fun getPackage(fqName: FqName): PackageViewDescriptor {
val originalPackageDescriptor = moduleDescriptor.getPackage(fqName)
if (fqName != FqName.ROOT) {
return originalPackageDescriptor
}
return object : DeclarationDescriptorImpl(Annotations.EMPTY, fqName.shortNameOrSpecial()), PackageViewDescriptor {
override fun getContainingDeclaration() = originalPackageDescriptor.containingDeclaration
override val fqName get() = originalPackageDescriptor.fqName
override val module get() = this@EvaluatorModuleDescriptor
override val memberScope by lazy {
if (fragments.isEmpty()) {
MemberScope.Empty
} else {
val scopes = fragments.map { it.getMemberScope() } + SubpackagesScope(module, fqName)
ChainedMemberScope("package view scope for $fqName in ${module.name}", scopes)
}
}
override val fragments by lazy { originalPackageDescriptor.fragments + packageFragmentForEvaluator }
override fun <R, D> accept(visitor: DeclarationDescriptorVisitor<R, D>, data: D): R {
return visitor.visitPackageViewDescriptor(this, data)
}
}
}
}
private val OutputFile.internalClassName: String
get() = relativePath.removeSuffix(".class").replace('/', '.')
@@ -0,0 +1,380 @@
/*
* 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.idea.debugger.evaluate.compilation
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.CodeFragmentCodegenInfo
import org.jetbrains.kotlin.codegen.getCallLabelForLambdaArgument
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor
import org.jetbrains.kotlin.descriptors.impl.SyntheticFieldDescriptor
import org.jetbrains.kotlin.idea.debugger.evaluate.DebuggerFieldPropertyDescriptor
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.CodeFragmentParameter.*
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory.Companion.FAKE_JAVA_CONTEXT_FUNCTION_NAME
import org.jetbrains.kotlin.idea.debugger.safeLocation
import org.jetbrains.kotlin.idea.debugger.safeMethod
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.sam.SingleAbstractMethodUtils
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.checkers.COROUTINE_CONTEXT_1_3_FQ_NAME
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.scopes.receivers.ExtensionReceiver
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.types.KotlinType
class CodeFragmentParameterInfo(
val parameters: List<Smart>,
val crossingBounds: Set<Dumb>
)
/*
The purpose of this class is to figure out what parameters the received code fragment captures.
It handles both directly mentioned names such as local variables or parameters and implicit values (dispatch/extension receivers).
*/
class CodeFragmentParameterAnalyzer(
private val context: ExecutionContext,
private val codeFragment: KtCodeFragment,
private val bindingContext: BindingContext
) {
private val parameters = LinkedHashMap<DeclarationDescriptor, Smart>()
private val crossingBounds = mutableSetOf<Dumb>()
private val onceUsedChecker = OnceUsedChecker(CodeFragmentParameterAnalyzer::class.java)
private val containingPrimaryConstructor: ConstructorDescriptor? by lazy {
context.frameProxy.safeLocation()?.safeMethod()?.takeIf { it.isConstructor } ?: return@lazy null
val constructor = codeFragment.context?.getParentOfType<KtPrimaryConstructor>(false) ?: return@lazy null
bindingContext[BindingContext.CONSTRUCTOR, constructor]
}
fun analyze(): CodeFragmentParameterInfo {
onceUsedChecker.trigger()
codeFragment.accept(object : KtTreeVisitor<Unit>() {
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression, data: Unit?): Void? {
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return null
processResolvedCall(resolvedCall, expression)
return null
}
private fun processResolvedCall(resolvedCall: ResolvedCall<*>, expression: KtSimpleNameExpression) {
// Capture dispatch receiver for the extension callable
run {
val descriptor = resolvedCall.resultingDescriptor
val containingClass = descriptor?.containingDeclaration as? ClassDescriptor
val extensionParameter = descriptor?.extensionReceiverParameter
if (descriptor != null && descriptor !is DebuggerFieldPropertyDescriptor
&& extensionParameter != null && containingClass != null
) {
if (containingClass.kind != ClassKind.OBJECT) {
processDispatchReceiver(containingClass)
}
}
}
if (runReadAction { expression.isDotSelector() }) {
// The receiver expression is already captured for this reference
return
}
if (isCodeFragmentDeclaration(resolvedCall.resultingDescriptor)) {
// The reference is from the code fragment we analyze, no need to capture
return
}
var processed = false
val extensionReceiver = resolvedCall.extensionReceiver
if (extensionReceiver is ImplicitReceiver) {
val descriptor = extensionReceiver.declarationDescriptor
val parameter = processReceiver(extensionReceiver)
checkBounds(descriptor, expression, parameter)
processed = true
}
val dispatchReceiver = resolvedCall.dispatchReceiver
if (dispatchReceiver is ImplicitReceiver) {
val descriptor = dispatchReceiver.declarationDescriptor
val parameter = processReceiver(dispatchReceiver)
if (parameter != null) {
checkBounds(descriptor, expression, parameter)
processed = true
}
}
if (!processed && resolvedCall.resultingDescriptor is SyntheticFieldDescriptor) {
val descriptor = resolvedCall.resultingDescriptor as SyntheticFieldDescriptor
val parameter = processSyntheticFieldVariable(descriptor)
if (parameter != null) {
checkBounds(descriptor, expression, parameter)
processed = true
}
}
// If a reference has receivers, we can calculate its value using them, no need to capture
if (!processed) {
if (resolvedCall is VariableAsFunctionResolvedCall) {
processResolvedCall(resolvedCall.functionCall, expression)
processResolvedCall(resolvedCall.variableCall, expression)
} else {
processDescriptor(resolvedCall.resultingDescriptor, expression)
}
}
}
private fun processDescriptor(descriptor: DeclarationDescriptor, expression: KtSimpleNameExpression) {
val parameter = processDebugLabel(descriptor)
?: processCoroutineContextCall(descriptor)
?: processSimpleNameExpression(descriptor)
checkBounds(descriptor, expression, parameter)
}
override fun visitThisExpression(expression: KtThisExpression, data: Unit?): Void? {
val instanceReference = runReadAction { expression.instanceReference }
val target = bindingContext[BindingContext.REFERENCE_TARGET, instanceReference]
if (isCodeFragmentDeclaration(target)) {
// The reference is from the code fragment we analyze, no need to capture
return null
}
val parameter = when (target) {
is ClassDescriptor -> processDispatchReceiver(target)
is CallableDescriptor -> {
val type = bindingContext[BindingContext.EXPRESSION_TYPE_INFO, expression]?.type
type?.let { processExtensionReceiver(target, type, expression.getLabelName()) }
}
else -> null
}
if (parameter != null) {
checkBounds(target, expression, parameter)
}
return null
}
override fun visitSuperExpression(expression: KtSuperExpression, data: Unit?): Void {
throw EvaluateExceptionUtil.createEvaluateException("Evaluation of 'super' call expression is not supported")
}
}, Unit)
return CodeFragmentParameterInfo(parameters.values.toList(), crossingBounds)
}
private fun processReceiver(receiver: ImplicitReceiver): Smart? {
return when (receiver) {
is ImplicitClassReceiver -> processDispatchReceiver(receiver.classDescriptor)
is ExtensionReceiver -> processExtensionReceiver(receiver.declarationDescriptor, receiver.type, null)
else -> null
}
}
private fun processDispatchReceiver(descriptor: ClassDescriptor): Smart? {
if (descriptor.kind == ClassKind.OBJECT || containingPrimaryConstructor != null) {
return null
}
val type = descriptor.defaultType
return parameters.getOrPut(descriptor) {
Smart(Dumb(Kind.DISPATCH_RECEIVER, "", AsmUtil.THIS + "@" + descriptor.name.asString()), type, descriptor)
}
}
private fun processExtensionReceiver(descriptor: CallableDescriptor, receiverType: KotlinType, label: String?): Smart? {
if (isFakeFunctionForJavaContext(descriptor)) {
return processFakeJavaCodeReceiver(descriptor)
}
val actualLabel = label ?: getLabel(descriptor) ?: return null
val receiverParameter = descriptor.extensionReceiverParameter ?: return null
return parameters.getOrPut(descriptor) {
Smart(Dumb(Kind.EXTENSION_RECEIVER, actualLabel, AsmUtil.THIS + "@" + actualLabel), receiverType, receiverParameter)
}
}
private fun getLabel(callableDescriptor: CallableDescriptor): String? {
val source = callableDescriptor.source.getPsi()
if (source is KtFunctionLiteral) {
getCallLabelForLambdaArgument(source, bindingContext)?.let { return it }
}
return callableDescriptor.name.takeIf { !it.isSpecial }?.asString()
}
private fun isFakeFunctionForJavaContext(descriptor: CallableDescriptor): Boolean {
return descriptor is FunctionDescriptor
&& descriptor.name.asString() == FAKE_JAVA_CONTEXT_FUNCTION_NAME
&& codeFragment.getCopyableUserData(KtCodeFragment.FAKE_CONTEXT_FOR_JAVA_FILE) != null
}
private fun processFakeJavaCodeReceiver(descriptor: CallableDescriptor): Smart? {
val receiverParameter = descriptor
.takeIf { descriptor is FunctionDescriptor }
?.extensionReceiverParameter
?: return null
val label = FAKE_JAVA_CONTEXT_FUNCTION_NAME
val type = receiverParameter.type
return parameters.getOrPut(descriptor) {
Smart(Dumb(Kind.FAKE_JAVA_OUTER_CLASS, label, AsmUtil.THIS), type, receiverParameter)
}
}
private fun processSyntheticFieldVariable(descriptor: SyntheticFieldDescriptor): Smart? {
val propertyDescriptor = descriptor.propertyDescriptor
val fieldName = propertyDescriptor.name.asString()
val type = propertyDescriptor.type
return parameters.getOrPut(descriptor) {
Smart(Dumb(Kind.FIELD_VAR, fieldName, "field"), type, descriptor)
}
}
private fun processSimpleNameExpression(target: DeclarationDescriptor): Smart? {
if (target is ValueParameterDescriptor && target.isCrossinline) {
throw EvaluateExceptionUtil.createEvaluateException("Evaluation of 'crossinline' lambdas is not supported")
}
val isLocalTarget = (target as? DeclarationDescriptorWithVisibility)?.visibility == Visibilities.LOCAL
val isPrimaryConstructorParameter = !isLocalTarget
&& target is PropertyDescriptor
&& isContainingPrimaryConstructorParameter(target)
if (!isLocalTarget && !isPrimaryConstructorParameter) {
return null
}
return when (target) {
is FunctionDescriptor -> {
val type = SingleAbstractMethodUtils.getFunctionTypeForAbstractMethod(target, false)
parameters.getOrPut(target) {
Smart(Dumb(Kind.LOCAL_FUNCTION, target.name.asString()), type, target)
}
}
is ValueDescriptor -> {
parameters.getOrPut(target) {
val type = target.type
@Suppress("DEPRECATION")
val kind = if (target is LocalVariableDescriptor && target.isDelegated) Kind.DELEGATED else Kind.ORDINARY
Smart(Dumb(kind, target.name.asString()), type, target)
}
}
else -> null
}
}
private fun isContainingPrimaryConstructorParameter(target: PropertyDescriptor): Boolean {
val primaryConstructor = containingPrimaryConstructor ?: return false
for (parameter in primaryConstructor.valueParameters) {
val property = bindingContext[BindingContext.VALUE_PARAMETER_AS_PROPERTY, parameter]
if (target == property) {
return true
}
}
return false
}
private fun processCoroutineContextCall(target: DeclarationDescriptor): Smart? {
if (target is PropertyDescriptor && target.fqNameSafe == COROUTINE_CONTEXT_1_3_FQ_NAME) {
return parameters.getOrPut(target) {
Smart(Dumb(Kind.COROUTINE_CONTEXT, ""), target.type, target)
}
}
return null
}
private fun processDebugLabel(target: DeclarationDescriptor): Smart? {
val debugLabelPropertyDescriptor = target as? DebugLabelPropertyDescriptor ?: return null
val labelName = debugLabelPropertyDescriptor.labelName
val debugString = debugLabelPropertyDescriptor.name.asString()
return parameters.getOrPut(target) {
val type = debugLabelPropertyDescriptor.type
Smart(Dumb(Kind.DEBUG_LABEL, labelName, debugString), type, debugLabelPropertyDescriptor)
}
}
fun checkBounds(descriptor: DeclarationDescriptor?, expression: KtExpression, parameter: Smart?) {
if (parameter == null || descriptor !is DeclarationDescriptorWithSource) {
return
}
val targetPsi = descriptor.source.getPsi()
if (targetPsi != null && doesCrossInlineBounds(expression, targetPsi)) {
crossingBounds += parameter.dumb
}
}
private fun doesCrossInlineBounds(expression: PsiElement, declaration: PsiElement): Boolean {
val declarationParent = declaration.parent ?: return false
var currentParent: PsiElement? = expression.parent?.takeIf { it.isInside(declarationParent) } ?: return false
while (currentParent != null && currentParent != declarationParent) {
if (currentParent is KtFunction) {
val functionDescriptor = bindingContext[BindingContext.FUNCTION, currentParent]
if (functionDescriptor != null && !functionDescriptor.isInline) {
return true
}
}
currentParent = when (currentParent) {
is KtCodeFragment -> currentParent.context
else -> currentParent.parent
}
}
return false
}
private fun isCodeFragmentDeclaration(descriptor: DeclarationDescriptor?): Boolean {
if (descriptor is ValueParameterDescriptor && isCodeFragmentDeclaration(descriptor.containingDeclaration)) {
return true
}
if (descriptor !is DeclarationDescriptorWithSource) {
return false
}
return descriptor.source.getPsi()?.containingFile is KtCodeFragment
}
private tailrec fun PsiElement.isInside(parent: PsiElement): Boolean {
if (parent.isAncestor(this)) {
return true
}
val context = (this.containingFile as? KtCodeFragment)?.context ?: return false
return context.isInside(parent)
}
}
private class OnceUsedChecker(private val clazz: Class<*>) {
private var used = false
fun trigger() {
if (used) {
error(clazz.name + " may be only used once")
}
used = true
}
}
@@ -0,0 +1,221 @@
/*
* 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.idea.debugger.evaluate.compilation
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.xdebugger.impl.XDebugSessionImpl
import com.intellij.xdebugger.impl.ui.tree.ValueMarkup
import org.jetbrains.kotlin.builtins.PrimitiveType
import com.sun.jdi.*
import org.jetbrains.kotlin.backend.common.SimpleMemberScope
import com.sun.jdi.Type as JdiType
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.impl.DeclarationDescriptorImpl
import org.jetbrains.kotlin.descriptors.impl.PackageFragmentDescriptorImpl
import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl
import org.jetbrains.kotlin.descriptors.impl.PropertyGetterDescriptorImpl
import org.jetbrains.kotlin.idea.debugger.getClassDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.psi.externalDescriptors
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.resolve.scopes.MemberScope
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.org.objectweb.asm.Type as AsmType
class DebugLabelPropertyDescriptorProvider(val codeFragment: KtCodeFragment, val debugProcess: DebugProcessImpl) {
companion object {
fun getMarkupMap(debugProcess: DebugProcessImpl) = doGetMarkupMap(debugProcess) ?: emptyMap()
private fun doGetMarkupMap(debugProcess: DebugProcessImpl): Map<out Value?, ValueMarkup>? {
if (ApplicationManager.getApplication().isUnitTestMode) {
return NodeDescriptorImpl.getMarkupMap(debugProcess)
}
val debugSession = debugProcess.session.xDebugSession as? XDebugSessionImpl
@Suppress("UNCHECKED_CAST")
return debugSession?.valueMarkers?.allMarkers?.filterKeys { it is Value? } as Map<out Value?, ValueMarkup>?
}
}
private val moduleDescriptor = DebugLabelModuleDescriptor
fun supplyDebugLabels() {
val packageFragment = object : PackageFragmentDescriptorImpl(moduleDescriptor, FqName.ROOT) {
val properties = createDebugLabelDescriptors(this)
override fun getMemberScope() = SimpleMemberScope(properties)
}
codeFragment.externalDescriptors = packageFragment.properties
}
private fun createDebugLabelDescriptors(containingDeclaration: PackageFragmentDescriptor): List<PropertyDescriptor> {
val markupMap = getMarkupMap(debugProcess)
val result = ArrayList<PropertyDescriptor>(markupMap.size)
nextValue@ for ((value, markup) in markupMap) {
val labelName = markup.text
val kotlinType = value?.type()?.let { convertType(it) } ?: moduleDescriptor.builtIns.nullableAnyType
result += createDebugLabelDescriptor(labelName, kotlinType, containingDeclaration)
}
return result
}
private fun createDebugLabelDescriptor(
labelName: String,
type: KotlinType,
containingDeclaration: PackageFragmentDescriptor
): PropertyDescriptor {
val propertyDescriptor = DebugLabelPropertyDescriptor(containingDeclaration, labelName)
propertyDescriptor.setType(type, emptyList(), null, null)
val getterDescriptor = PropertyGetterDescriptorImpl(
propertyDescriptor,
Annotations.EMPTY,
Modality.FINAL,
Visibilities.PUBLIC,
/* isDefault = */ false,
/* isExternal = */ false,
/* isInline = */ false,
CallableMemberDescriptor.Kind.SYNTHESIZED,
/* original = */ null,
SourceElement.NO_SOURCE
).apply { initialize(type) }
propertyDescriptor.initialize(getterDescriptor, null)
return propertyDescriptor
}
private fun convertType(type: JdiType): KotlinType {
val builtIns = moduleDescriptor.builtIns
return when (type) {
is VoidType -> builtIns.unitType
is LongType -> builtIns.longType
is DoubleType -> builtIns.doubleType
is CharType -> builtIns.charType
is FloatType -> builtIns.floatType
is ByteType -> builtIns.byteType
is IntegerType -> builtIns.intType
is BooleanType -> builtIns.booleanType
is ShortType -> builtIns.shortType
is ArrayType -> {
when (val componentType = type.componentType()) {
is VoidType -> builtIns.getArrayType(Variance.INVARIANT, builtIns.unitType)
is LongType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.LONG)
is DoubleType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.DOUBLE)
is CharType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.CHAR)
is FloatType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.FLOAT)
is ByteType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.BYTE)
is IntegerType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.INT)
is BooleanType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.BOOLEAN)
is ShortType -> builtIns.getPrimitiveArrayKotlinType(PrimitiveType.SHORT)
else -> builtIns.getArrayType(Variance.INVARIANT, convertReferenceType(componentType))
}
}
is ReferenceType -> convertReferenceType(type)
else -> builtIns.anyType
}
}
private fun convertReferenceType(type: JdiType): KotlinType {
require(type is ClassType || type is InterfaceType)
val asmType = AsmType.getType(type.signature())
val project = codeFragment.project
val classDescriptor = asmType.getClassDescriptor(GlobalSearchScope.allScope(project), mapBuiltIns = false)
?: return moduleDescriptor.builtIns.nullableAnyType
return classDescriptor.defaultType
}
}
private object DebugLabelModuleDescriptor
: DeclarationDescriptorImpl(Annotations.EMPTY, Name.identifier("DebugLabelExtensions")),
ModuleDescriptor
{
override val builtIns: KotlinBuiltIns
get() = DefaultBuiltIns.Instance
override val stableName: Name?
get() = name
override fun shouldSeeInternalsOf(targetModule: ModuleDescriptor) = false
override fun getPackage(fqName: FqName): PackageViewDescriptor {
return object : PackageViewDescriptor, DeclarationDescriptorImpl(Annotations.EMPTY, FqName.ROOT.shortNameOrSpecial()) {
override fun getContainingDeclaration(): PackageViewDescriptor? = null
override val fqName: FqName
get() = FqName.ROOT
override val memberScope: MemberScope
get() = MemberScope.Empty
override val module: ModuleDescriptor
get() = this@DebugLabelModuleDescriptor
override val fragments: List<PackageFragmentDescriptor>
get() = emptyList()
override fun <R : Any?, D : Any?> accept(visitor: DeclarationDescriptorVisitor<R, D>, data: D): R {
return visitor.visitPackageViewDescriptor(this, data)
}
}
}
override val platform: TargetPlatform?
get() = JvmPlatforms.unspecifiedJvmPlatform
override fun getSubPackagesOf(fqName: FqName, nameFilter: (Name) -> Boolean): Collection<FqName> {
return emptyList()
}
override val allDependencyModules: List<ModuleDescriptor>
get() = emptyList()
override val expectedByModules: List<ModuleDescriptor>
get() = emptyList()
override fun <T> getCapability(capability: ModuleDescriptor.Capability<T>): T? = null
override val isValid: Boolean
get() = true
override fun assertValid() {}
}
internal class DebugLabelPropertyDescriptor(
containingDeclaration: DeclarationDescriptor,
val labelName: String
) : PropertyDescriptorImpl(
containingDeclaration,
null,
Annotations.EMPTY,
Modality.FINAL,
Visibilities.PUBLIC,
/*isVar = */false,
Name.identifier(labelName + "_DebugLabel"),
CallableMemberDescriptor.Kind.SYNTHESIZED,
SourceElement.NO_SOURCE,
/*lateInit = */false,
/*isConst = */false,
/*isExpect = */false,
/*isActual = */false,
/*isExternal = */false,
/*isDelegated = */false
)
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.sun.jdi.ClassLoaderReference
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.LOG
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoadingAdapter
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
fun loadClassesSafely(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference? {
return try {
loadClasses(context, classes)
} catch (e: EvaluateException) {
throw e
} catch (e: Throwable) {
LOG.debug("Failed to evaluate expression", e)
null
}
}
fun loadClasses(context: ExecutionContext, classes: Collection<ClassToLoad>): ClassLoaderReference? {
if (classes.isEmpty()) {
return null
}
return ClassLoadingAdapter.loadClasses(context, classes)
}
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.debugger.evaluate.surroundWith;
import com.intellij.lang.surroundWith.Surrounder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.idea.core.surroundWith.KotlinExpressionSurroundDescriptorBase;
import org.jetbrains.kotlin.idea.debugger.surroundWith.KotlinRuntimeTypeCastSurrounder;
public class KotlinDebuggerExpressionSurroundDescriptor extends KotlinExpressionSurroundDescriptorBase {
private static final Surrounder[] SURROUNDERS = {
new KotlinRuntimeTypeCastSurrounder()
};
@Override
@NotNull
public Surrounder[] getSurrounders() {
return SURROUNDERS;
}
}
@@ -0,0 +1,104 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. 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.idea.debugger.surroundWith
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.openapi.application.Result
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.util.ProgressWindow
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinRuntimeTypeEvaluator
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.surroundWith.KotlinExpressionSurrounder
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.checker.KotlinTypeChecker
class KotlinRuntimeTypeCastSurrounder: KotlinExpressionSurrounder() {
override fun isApplicable(expression: KtExpression): Boolean {
if (!super.isApplicable(expression)) return false
if (!expression.isPhysical) return false
val file = expression.containingFile
if (file !is KtCodeFragment) return false
val type = expression.analyze(BodyResolveMode.PARTIAL).getType(expression) ?: return false
return TypeUtils.canHaveSubtypes(KotlinTypeChecker.DEFAULT, type)
}
override fun surroundExpression(project: Project, editor: Editor, expression: KtExpression): TextRange? {
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
val debuggerSession = debuggerContext.debuggerSession
if (debuggerSession != null) {
val progressWindow = ProgressWindow(true, expression.project)
val worker = SurroundWithCastWorker(editor, expression, debuggerContext, progressWindow)
progressWindow.title = DebuggerBundle.message("title.evaluating")
debuggerContext.debugProcess?.managerThread?.startProgress(worker, progressWindow)
}
return null
}
override fun getTemplateDescription(): String {
return KotlinBundle.message("surround.with.runtime.type.cast.template")
}
private inner class SurroundWithCastWorker(
private val myEditor: Editor,
expression: KtExpression,
context: DebuggerContextImpl,
indicator: ProgressIndicator
): KotlinRuntimeTypeEvaluator(myEditor, expression, context, indicator) {
override fun typeCalculationFinished(type: KotlinType?) {
if (type == null) return
hold()
val project = myEditor.project
DebuggerInvocationUtil.invokeLater(project, Runnable {
object : WriteCommandAction<Any>(project, CodeInsightBundle.message("command.name.surround.with.runtime.cast")) {
override fun run(result: Result<Any>) {
try {
val factory = KtPsiFactory(myElement.project)
val fqName = DescriptorUtils.getFqName(type.constructor.declarationDescriptor!!)
val parentCast = factory.createExpression("(expr as " + fqName.asString() + ")") as KtParenthesizedExpression
val cast = parentCast.expression as KtBinaryExpressionWithTypeRHS
cast.left.replace(myElement)
val expr = myElement.replace(parentCast) as KtExpression
ShortenReferences.DEFAULT.process(expr)
val range = expr.textRange
myEditor.selectionModel.setSelection(range.startOffset, range.endOffset)
myEditor.caretModel.moveToOffset(range.endOffset)
myEditor.scrollingModel.scrollToCaret(ScrollType.RELATIVE)
}
finally {
release()
}
}
}.execute()
}, myProgressIndicator.modalityState)
}
}
}
@@ -0,0 +1,264 @@
/*
* 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.idea.debugger.evaluate.variables
import com.sun.jdi.*
import org.jetbrains.kotlin.fileClasses.internalNameWithoutInnerClasses
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.variables.VariableFinder.Result
import org.jetbrains.kotlin.idea.debugger.isSubtype
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType
import org.jetbrains.org.objectweb.asm.Type as AsmType
import com.sun.jdi.Type as JdiType
import kotlin.jvm.internal.Ref
@Suppress("SpellCheckingInspection")
class EvaluatorValueConverter(private val context: ExecutionContext) {
private companion object {
private val UNBOXING_METHOD_NAMES = mapOf(
"java/lang/Boolean" to "booleanValue",
"java/lang/Character" to "charValue",
"java/lang/Byte" to "byteValue",
"java/lang/Short" to "shortValue",
"java/lang/Integer" to "intValue",
"java/lang/Float" to "floatValue",
"java/lang/Long" to "longValue",
"java/lang/Double" to "doubleValue"
)
}
// Nearly accurate: doesn't do deep checks for Ref wrappers. Use `coerce()` for more precise check.
fun typeMatches(requestedType: AsmType, actualTypeObj: JdiType?): Boolean {
if (actualTypeObj == null) return true
// Main path
if (requestedType.descriptor == "Ljava/lang/Object;" || actualTypeObj.isSubtype(requestedType)) {
return true
}
val actualType = actualTypeObj.asmType()
fun isRefWrapper(wrapperType: AsmType, objType: AsmType): Boolean {
return !objType.isPrimitiveType && wrapperType.className == Ref.ObjectRef::class.java.name
}
if (isRefWrapper(actualType, requestedType) || isRefWrapper(requestedType, actualType)) {
return true
}
val unwrappedActualType = unwrap(actualType)
val unwrappedRequestedType = unwrap(requestedType)
return unwrappedActualType == unwrappedRequestedType
}
fun coerce(value: Value?, type: AsmType): Result? {
val unrefResult = coerceRef(value, type) ?: return null
return coerceBoxing(unrefResult.value, type)
}
private fun coerceRef(value: Value?, type: AsmType): Result? {
when {
type.isRefType -> {
if (value != null && value.asmType().isRefType) {
return Result(value)
}
return Result(ref(value))
}
value != null && value.asmType().isRefType -> {
if (type.isRefType) {
return Result(value)
}
return Result(unref(value))
}
else -> return Result(value)
}
}
private fun coerceBoxing(value: Value?, type: AsmType): Result? {
when {
value == null -> return Result(value)
type == AsmType.VOID_TYPE -> return Result(context.vm.mirrorOfVoid())
type.isBoxedType -> {
if (value.asmType().isBoxedType) {
return Result(value)
}
if (value !is PrimitiveValue) {
return null
}
return Result(box(value))
}
type.isPrimitiveType -> {
if (value is PrimitiveValue) {
return Result(value)
}
if (value !is ObjectReference || !value.asmType().isBoxedType) {
return null
}
return Result(unbox(value))
}
value is PrimitiveValue -> {
if (type.sort != AsmType.OBJECT) {
return null
}
val boxedValue = box(value)
if (!typeMatches(type, boxedValue?.type())) {
return null
}
return Result(boxedValue)
}
else -> return Result(value)
}
}
private fun box(value: Value?): Value? {
if (value !is PrimitiveValue) {
return value
}
val unboxedType = value.asmType()
val boxedType = box(unboxedType)
val boxedTypeClass = (context.findClass(boxedType) as ClassType?)
?: error("Class $boxedType is not loaded")
val methodDesc = AsmType.getMethodDescriptor(boxedType, unboxedType)
val valueOfMethod = boxedTypeClass.methodsByName("valueOf", methodDesc).first()
return context.invokeMethod(boxedTypeClass, valueOfMethod, listOf(value))
}
private fun unbox(value: Value?): Value? {
if (value !is ObjectReference) {
return value
}
val boxedTypeClass = value.referenceType() as? ClassType ?: return value
val boxedType = boxedTypeClass.asmType().takeIf { it.isBoxedType } ?: return value
val unboxedType = unbox(boxedType)
val unboxingMethodName = UNBOXING_METHOD_NAMES.getValue(boxedType.internalName)
val methodDesc = AsmType.getMethodDescriptor(unboxedType)
val valueMethod = boxedTypeClass.methodsByName(unboxingMethodName, methodDesc).first()
return context.invokeMethod(value, valueMethod, emptyList())
}
private fun ref(value: Value?): Value? {
if (value is VoidValue) {
return value
}
fun wrapRef(value: Value?, refTypeClass: ClassType): Value? {
val constructor = refTypeClass.methods().single { it.isConstructor }
val ref = context.newInstance(refTypeClass, constructor, emptyList())
context.keepReference(ref)
val elementField = refTypeClass.fieldByName("element") ?: error("'element' field not found")
ref.setValue(elementField, value)
return ref
}
if (value is PrimitiveValue) {
val primitiveType = value.asmType()
val refType = PRIMITIVE_TO_REF.getValue(primitiveType)
val refTypeClass = (context.findClass(refType) as ClassType?)
?: error("Class $refType is not loaded")
return wrapRef(value, refTypeClass)
} else {
val refType = AsmType.getType(Ref.ObjectRef::class.java)
val refTypeClass = (context.findClass(refType) as ClassType?)
?: error("Class $refType is not loaded")
return wrapRef(value, refTypeClass)
}
}
fun unref(value: Value?): Value? {
if (value !is ObjectReference) {
return value
}
val type = value.type()
if (type !is ClassType || !type.signature().startsWith("L" + AsmTypes.REF_TYPE_PREFIX)) {
return value
}
val field = type.fieldByName("element") ?: return value
return value.getValue(field)
}
}
private fun unbox(type: AsmType): AsmType {
if (type.sort == AsmType.OBJECT) {
return BOXED_TO_PRIMITIVE[type] ?: type
}
return type
}
private fun box(type: AsmType): AsmType {
if (type.isPrimitiveType) {
return PRIMITIVE_TO_BOXED[type] ?: type
}
return type
}
private fun unwrap(type: AsmType): AsmType {
if (type.sort != AsmType.OBJECT) {
return type
}
return REF_TO_PRIMITIVE[type] ?: BOXED_TO_PRIMITIVE[type] ?: type
}
private val AsmType.isPrimitiveType: Boolean
get() = sort != AsmType.OBJECT && sort != AsmType.ARRAY
private val AsmType.isRefType: Boolean
get() = sort == AsmType.OBJECT && this in REF_TYPES
private val AsmType.isBoxedType: Boolean
get() = this in BOXED_TO_PRIMITIVE
private fun Value.asmType(): AsmType {
return type().asmType()
}
private fun JdiType.asmType(): AsmType {
return AsmType.getType(signature())
}
private val BOXED_TO_PRIMITIVE: Map<AsmType, AsmType> = JvmPrimitiveType.values()
.map { Pair(AsmType.getObjectType(it.wrapperFqName.internalNameWithoutInnerClasses), AsmType.getType(it.desc)) }
.toMap()
private val PRIMITIVE_TO_BOXED: Map<AsmType, AsmType> = BOXED_TO_PRIMITIVE.map { (k, v) -> Pair(v, k) }.toMap()
private val REF_TO_PRIMITIVE = mapOf(
Ref.ByteRef::class.java.name to AsmType.BYTE_TYPE,
Ref.ShortRef::class.java.name to AsmType.SHORT_TYPE,
Ref.IntRef::class.java.name to AsmType.INT_TYPE,
Ref.LongRef::class.java.name to AsmType.LONG_TYPE,
Ref.FloatRef::class.java.name to AsmType.FLOAT_TYPE,
Ref.DoubleRef::class.java.name to AsmType.DOUBLE_TYPE,
Ref.CharRef::class.java.name to AsmType.CHAR_TYPE,
Ref.BooleanRef::class.java.name to AsmType.BOOLEAN_TYPE
).mapKeys { (k, _) -> AsmType.getObjectType(k.replace('.', '/')) }
private val PRIMITIVE_TO_REF: Map<AsmType, AsmType> = REF_TO_PRIMITIVE.map { (k, v) -> Pair(v, k) }.toMap()
private val REF_TYPES: Set<AsmType> = REF_TO_PRIMITIVE.keys + AsmType.getType(Ref.ObjectRef::class.java)
@@ -0,0 +1,466 @@
/*
* 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.idea.debugger.evaluate.variables
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.openapi.diagnostic.Attachment
import com.sun.jdi.*
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.AsmUtil.getCapturedFieldName
import org.jetbrains.kotlin.codegen.AsmUtil.getLabeledThisName
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.codegen.inline.INLINE_TRANSFORMATION_SUFFIX
import org.jetbrains.kotlin.idea.core.util.mergeAttachments
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.LOG
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.CodeFragmentParameter
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.CodeFragmentParameter.*
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.DebugLabelPropertyDescriptorProvider
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import kotlin.coroutines.Continuation
import org.jetbrains.org.objectweb.asm.Type as AsmType
import com.sun.jdi.Type as JdiType
class VariableFinder(private val context: ExecutionContext) {
private val frameProxy = context.frameProxy
companion object {
private const val USE_UNSAFE_FALLBACK = true
fun variableNotFound(context: ExecutionContext, message: String): Exception {
val frameProxy = context.frameProxy
val location = frameProxy.safeLocation()
val scope = context.debugProcess.searchScope
val locationText = location?.run { "Location: ${sourceName()}:${lineNumber()}" } ?: "No location available"
val sourceName = location?.sourceName()
val declaringTypeName = location?.declaringType()?.name()?.replace('.', '/')?.let { JvmClassName.byInternalName(it) }
val sourceFile = if (sourceName != null && declaringTypeName != null) {
DebuggerUtils.findSourceFileForClassIncludeLibrarySources(context.project, scope, declaringTypeName, sourceName, location)
} else {
null
}
val sourceFileText = runReadAction { sourceFile?.text }
if (sourceName != null && sourceFileText != null) {
val attachments = mergeAttachments(
Attachment(sourceName, sourceFileText),
Attachment("location.txt", locationText)
)
LOG.error(message, attachments)
}
return EvaluateExceptionUtil.createEvaluateException(message)
}
private fun getCapturedVariableNameRegex(capturedName: String): Regex {
val escapedName = Regex.escape(capturedName)
val escapedSuffix = Regex.escape(INLINE_TRANSFORMATION_SUFFIX)
return Regex("^$escapedName(?:$escapedSuffix)?$")
}
}
private val evaluatorValueConverter = EvaluatorValueConverter(context)
sealed class VariableKind(val asmType: AsmType) {
abstract fun capturedNameMatches(name: String): Boolean
class Ordinary(val name: String, asmType: AsmType, val isDelegated: Boolean) : VariableKind(asmType) {
private val capturedNameRegex = getCapturedVariableNameRegex(getCapturedFieldName(this.name))
override fun capturedNameMatches(name: String) = capturedNameRegex.matches(name)
}
// TODO Support overloaded local functions
class LocalFunction(val name: String, asmType: AsmType) : VariableKind(asmType) {
@Suppress("ConvertToStringTemplate")
override fun capturedNameMatches(name: String) = name == "$" + name
}
class UnlabeledThis(asmType: AsmType) : VariableKind(asmType) {
override fun capturedNameMatches(name: String) =
(name == AsmUtil.CAPTURED_RECEIVER_FIELD || name.startsWith(getCapturedFieldName(AsmUtil.LABELED_THIS_FIELD)))
}
class OuterClassThis(asmType: AsmType) : VariableKind(asmType) {
override fun capturedNameMatches(name: String) = false
}
class FieldVar(val fieldName: String, asmType: AsmType) : VariableKind(asmType) {
// Captured 'field' are not supported yet
override fun capturedNameMatches(name: String) = false
}
class ExtensionThis(val label: String, asmType: AsmType) : VariableKind(asmType) {
val parameterName = getLabeledThisName(label, AsmUtil.LABELED_THIS_PARAMETER, AsmUtil.RECEIVER_PARAMETER_NAME)
val fieldName = getLabeledThisName(label, getCapturedFieldName(AsmUtil.LABELED_THIS_FIELD), AsmUtil.CAPTURED_RECEIVER_FIELD)
private val capturedNameRegex = getCapturedVariableNameRegex(fieldName)
override fun capturedNameMatches(name: String) = capturedNameRegex.matches(name)
}
}
class Result(val value: Value?)
private class NamedEntity(val name: String, val type: JdiType?, val value: () -> Value?) {
companion object {
fun of(field: Field, owner: ObjectReference): NamedEntity {
return NamedEntity(field.name(), field.safeType()) { owner.getValue(field) }
}
fun of(variable: LocalVariableProxyImpl, frameProxy: StackFrameProxyImpl): NamedEntity {
return NamedEntity(variable.name(), variable.safeType()) { frameProxy.getValue(variable) }
}
}
}
fun find(parameter: CodeFragmentParameter, asmType: AsmType): Result? {
return when (parameter.kind) {
Kind.ORDINARY -> findOrdinary(VariableKind.Ordinary(parameter.name, asmType, isDelegated = false))
Kind.DELEGATED -> findOrdinary(VariableKind.Ordinary(parameter.name, asmType, isDelegated = true))
Kind.FAKE_JAVA_OUTER_CLASS -> thisObject()?.let { Result(it) }
Kind.EXTENSION_RECEIVER -> findExtensionThis(VariableKind.ExtensionThis(parameter.name, asmType))
Kind.LOCAL_FUNCTION -> findLocalFunction(VariableKind.LocalFunction(parameter.name, asmType))
Kind.DISPATCH_RECEIVER -> findDispatchThis(VariableKind.OuterClassThis(asmType))
Kind.COROUTINE_CONTEXT -> findCoroutineContext()
Kind.FIELD_VAR -> findFieldVariable(VariableKind.FieldVar(parameter.name, asmType))
Kind.DEBUG_LABEL -> findDebugLabel(parameter.name)
}
}
private fun findOrdinary(kind: VariableKind.Ordinary): Result? {
val variables = frameProxy.safeVisibleVariables()
// Local variables direct search
findLocalVariable(variables, kind, kind.name)?.let { return it }
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
// Recursive search in captured this
val containingThis = thisObject() ?: return null
return findCapturedVariable(kind, containingThis)
}
private fun findFieldVariable(kind: VariableKind.FieldVar): Result? {
val thisObject = thisObject()
if (thisObject != null) {
val field = thisObject.referenceType().fieldByName(kind.fieldName) ?: return null
return Result(thisObject.getValue(field))
} else {
val containingType = frameProxy.safeLocation()?.declaringType() ?: return null
val field = containingType.fieldByName(kind.fieldName) ?: return null
return Result(containingType.getValue(field))
}
}
private fun findLocalFunction(kind: VariableKind.LocalFunction): Result? {
val variables = frameProxy.safeVisibleVariables()
// Local variables direct search, new convention
val newConventionName = AsmUtil.LOCAL_FUNCTION_VARIABLE_PREFIX + kind.name
findLocalVariable(variables, kind, newConventionName)?.let { return it }
// Local variables direct search, old convention (before 1.3.30)
findLocalVariable(variables, kind, kind.name + "$")?.let { return it }
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
// Recursive search in captured this
val containingThis = thisObject() ?: return null
return findCapturedVariable(kind, containingThis)
}
private fun findExtensionThis(kind: VariableKind.ExtensionThis): Result? {
val variables = frameProxy.safeVisibleVariables()
// Local variables direct search
val namePredicate = fun(name: String) = name == kind.parameterName || name.startsWith(kind.parameterName + '$')
findLocalVariable(variables, kind, namePredicate)?.let { return it }
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
// Recursive search in captured this
val containingThis = thisObject()
if (containingThis != null) {
findCapturedVariable(kind, containingThis)?.let { return it }
}
@Suppress("ConstantConditionIf")
if (USE_UNSAFE_FALLBACK) {
// Find an unlabeled this with the compatible type
findUnlabeledThis(VariableKind.UnlabeledThis(kind.asmType))?.let { return it }
}
return null
}
private fun findDispatchThis(kind: VariableKind.OuterClassThis): Result? {
val containingThis = thisObject()
if (containingThis != null) {
findCapturedVariable(kind, containingThis)?.let { return it }
}
if (isInsideDefaultImpls()) {
val variables = frameProxy.safeVisibleVariables()
findLocalVariable(variables, kind, AsmUtil.THIS_IN_DEFAULT_IMPLS)?.let { return it }
}
val variables = frameProxy.safeVisibleVariables()
val inlineDepth = getInlineDepth(variables)
if (inlineDepth > 0) {
variables.namedEntitySequence()
.filter { it.name.matches(INLINED_THIS_REGEX) && getInlineDepth(it.name) == inlineDepth && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
}
@Suppress("ConstantConditionIf")
if (USE_UNSAFE_FALLBACK) {
// Find an unlabeled this with the compatible type
findUnlabeledThis(VariableKind.UnlabeledThis(kind.asmType))?.let { return it }
}
return null
}
private fun findDebugLabel(name: String): Result? {
val markupMap = DebugLabelPropertyDescriptorProvider.getMarkupMap(context.debugProcess)
for ((value, markup) in markupMap) {
if (markup.text == name) {
return Result(value)
}
}
return null
}
private fun findUnlabeledThis(kind: VariableKind.UnlabeledThis): Result? {
val variables = frameProxy.safeVisibleVariables()
// Recursive search in local receiver variables
findCapturedVariableInReceiver(variables, kind)?.let { return it }
val containingThis = thisObject() ?: return null
return findCapturedVariable(kind, containingThis)
}
private fun findLocalVariable(variables: List<LocalVariableProxyImpl>, kind: VariableKind, name: String): Result? {
return findLocalVariable(variables, kind) { it == name }
}
private fun findLocalVariable(
variables: List<LocalVariableProxyImpl>,
kind: VariableKind,
namePredicate: (String) -> Boolean
): Result? {
val inlineDepth = getInlineDepth(variables)
if (inlineDepth > 0) {
val inlineAwareNamePredicate = fun(name: String): Boolean {
var endIndex = name.length
var depth = 0
val suffixLen = INLINE_FUN_VAR_SUFFIX.length
while (endIndex >= suffixLen) {
if (name.substring(endIndex - suffixLen, endIndex) != INLINE_FUN_VAR_SUFFIX) {
break
}
depth++
endIndex -= suffixLen
}
return namePredicate(name.take(endIndex))
}
variables.namedEntitySequence()
.filter { inlineAwareNamePredicate(it.name) && getInlineDepth(it.name) == inlineDepth && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
}
variables.namedEntitySequence()
.filter { namePredicate(it.name) && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
return null
}
private fun isInsideDefaultImpls(): Boolean {
val declaringType = frameProxy.safeLocation()?.declaringType() ?: return false
return declaringType.name().endsWith(JvmAbi.DEFAULT_IMPLS_SUFFIX)
}
private fun findCoroutineContext(): Result? {
val method = frameProxy.safeLocation()?.safeMethod() ?: return null
val result = findCoroutineContextForLambda(method) ?: findCoroutineContextForMethod(method) ?: return null
return Result(result)
}
private fun findCoroutineContextForLambda(method: Method): ObjectReference? {
if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") {
return null
}
val thisObject = thisObject() ?: return null
val thisType = thisObject.referenceType()
if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) {
return null
}
return findCoroutineContextForContinuation(thisObject)
}
private fun findCoroutineContextForMethod(method: Method): ObjectReference? {
if (CONTINUATION_TYPE.descriptor + ")" !in method.signature()) {
return null
}
val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null
val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null
return findCoroutineContextForContinuation(continuation)
}
private fun findCoroutineContextForContinuation(continuation: ObjectReference): ObjectReference? {
val continuationType = (continuation.referenceType() as? ClassType)
?.allInterfaces()?.firstOrNull { it.name() == Continuation::class.java.name }
?: return null
val getContextMethod = continuationType
.methodsByName("getContext", "()Lkotlin/coroutines/CoroutineContext;").firstOrNull()
?: return null
return context.invokeMethod(continuation, getContextMethod, emptyList()) as? ObjectReference
}
private fun findCapturedVariableInReceiver(variables: List<LocalVariableProxyImpl>, kind: VariableKind): Result? {
fun isReceiverOrPassedThis(name: String) =
name.startsWith(AsmUtil.LABELED_THIS_PARAMETER)
|| name == AsmUtil.RECEIVER_PARAMETER_NAME
|| name == AsmUtil.THIS_IN_DEFAULT_IMPLS
|| INLINED_THIS_REGEX.matches(name)
if (kind is VariableKind.ExtensionThis) {
variables.namedEntitySequence()
.filter { kind.capturedNameMatches(it.name) && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
}
return variables.namedEntitySequence()
.filter { isReceiverOrPassedThis(it.name) }
.mapNotNull { findCapturedVariable(kind, it.value) }
.firstOrNull()
}
private fun findCapturedVariable(kind: VariableKind, parentFactory: () -> Value?): Result? {
val parent = getUnwrapDelegate(kind, parentFactory)
return findCapturedVariable(kind, parent)
}
private fun findCapturedVariable(kind: VariableKind, parent: Value?): Result? {
val acceptsParentValue = kind is VariableKind.UnlabeledThis || kind is VariableKind.OuterClassThis
if (parent != null && acceptsParentValue && kind.typeMatches(parent.type())) {
return Result(parent)
}
val fields = (parent as? ObjectReference)?.referenceType()?.fields() ?: return null
if (kind !is VariableKind.OuterClassThis) {
// Captured variables - direct search
fields.namedEntitySequence(parent)
.filter { kind.capturedNameMatches(it.name) && kind.typeMatches(it.type) }
.mapNotNull { it.unwrapAndCheck(kind) }
.firstOrNull()
?.let { return it }
// Recursive search in captured receivers
fields.namedEntitySequence(parent)
.filter { isCapturedReceiverFieldName(it.name) }
.mapNotNull { findCapturedVariable(kind, it.value) }
.firstOrNull()
?.let { return it }
}
// Recursive search in outer and captured this
fields.namedEntitySequence(parent)
.filter { it.name == AsmUtil.THIS_IN_DEFAULT_IMPLS || it.name == AsmUtil.CAPTURED_THIS_FIELD }
.mapNotNull { findCapturedVariable(kind, it.value) }
.firstOrNull()
?.let { return it }
return null
}
private fun getUnwrapDelegate(kind: VariableKind, valueFactory: () -> Value?): Value? {
val rawValue = valueFactory()
if (kind !is VariableKind.Ordinary || !kind.isDelegated) {
return rawValue
}
val delegateValue = rawValue as? ObjectReference ?: return rawValue
val getValueMethod = delegateValue.referenceType()
.methodsByName("getValue", "()Ljava/lang/Object;").firstOrNull()
?: return rawValue
return context.invokeMethod(delegateValue, getValueMethod, emptyList())
}
private fun isCapturedReceiverFieldName(name: String): Boolean {
return name.startsWith(getCapturedFieldName(AsmUtil.LABELED_THIS_FIELD))
|| name == AsmUtil.CAPTURED_RECEIVER_FIELD
}
private fun VariableKind.typeMatches(actualType: JdiType?): Boolean {
if (this is VariableKind.Ordinary && isDelegated) {
// We can't figure out the actual type of the value yet.
// No worries: it will be checked again (and more carefully) in `unwrapAndCheck()`.
return true
}
return evaluatorValueConverter.typeMatches(asmType, actualType)
}
private fun NamedEntity.unwrapAndCheck(kind: VariableKind): Result? {
return evaluatorValueConverter.coerce(getUnwrapDelegate(kind, value), kind.asmType)
}
private fun List<Field>.namedEntitySequence(owner: ObjectReference): Sequence<NamedEntity> {
return asSequence().map { NamedEntity.of(it, owner) }
}
private fun List<LocalVariableProxyImpl>.namedEntitySequence(): Sequence<NamedEntity> {
return asSequence().map { NamedEntity.of(it, frameProxy) }
}
private fun thisObject(): ObjectReference? {
val thisObjectFromEvaluation = context.evaluationContext.computeThisObject() as? ObjectReference
if (thisObjectFromEvaluation != null) {
return thisObjectFromEvaluation
}
return frameProxy.thisObject()
}
}
@@ -0,0 +1,20 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
compile(project(":compiler:backend"))
compile(project(":idea:ide-common"))
compile(files("${System.getProperty("java.home")}/../lib/tools.jar"))
compileOnly(intellijPluginDep("stream-debugger"))
testCompile(project(":kotlin-test:kotlin-test-junit"))
testCompile(commonDep("junit:junit"))
}
sourceSets {
"main" { projectDefault() }
"test" { none() }
}
@@ -0,0 +1,54 @@
/*
* Copyright 2000-2017 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.idea.debugger.sequence.lib.collections
import com.intellij.debugger.streams.lib.IntermediateOperation
import com.intellij.debugger.streams.lib.TerminalOperation
import com.intellij.debugger.streams.lib.impl.LibrarySupportBase
import com.intellij.debugger.streams.resolve.FilterResolver
import com.intellij.debugger.streams.resolve.ValuesOrderResolver
import com.intellij.debugger.streams.trace.CallTraceInterpreter
import com.intellij.debugger.streams.trace.IntermediateCallHandler
import com.intellij.debugger.streams.trace.TerminatorCallHandler
import com.intellij.debugger.streams.trace.dsl.Dsl
import com.intellij.debugger.streams.wrapper.IntermediateStreamCall
import com.intellij.debugger.streams.wrapper.TerminatorStreamCall
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.handler.collections.BothSemanticHandlerWrapper
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.handler.collections.BothSemanticsHandler
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.handler.collections.FilterCallHandler
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.interpret.FilterTraceInterpreter
class KotlinCollectionLibrarySupport : LibrarySupportBase() {
init {
addOperation(FilterOperation("filter", FilterCallHandler(), true))
addOperation(FilterOperation("filterNot", FilterCallHandler(), false))
}
private fun addOperation(operation: CollectionOperation) {
addIntermediateOperationsSupport(operation)
addTerminationOperationsSupport(operation)
}
private abstract class CollectionOperation(
override val name: String,
handler: BothSemanticsHandler
) : IntermediateOperation, TerminalOperation {
private val wrapper = BothSemanticHandlerWrapper(handler)
override fun getTraceHandler(callOrder: Int, call: IntermediateStreamCall, dsl: Dsl): IntermediateCallHandler =
wrapper.createIntermediateHandler(callOrder, call, dsl)
override fun getTraceHandler(call: TerminatorStreamCall, resultExpression: String, dsl: Dsl): TerminatorCallHandler =
wrapper.createTerminatorHandler(call, resultExpression, dsl)
}
private class FilterOperation(name: String, handler: BothSemanticsHandler, valueToAccept: Boolean) :
CollectionOperation(name, handler) {
override val traceInterpreter: CallTraceInterpreter = FilterTraceInterpreter(valueToAccept)
override val valuesOrderResolver: ValuesOrderResolver = FilterResolver()
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2000-2017 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.idea.debugger.sequence.lib.collections
import com.intellij.debugger.streams.lib.LibrarySupport
import com.intellij.debugger.streams.lib.LibrarySupportProvider
import com.intellij.debugger.streams.trace.TraceExpressionBuilder
import com.intellij.debugger.streams.trace.dsl.impl.DslImpl
import com.intellij.debugger.streams.wrapper.StreamChainBuilder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.debugger.sequence.psi.collections.KotlinCollectionChainBuilder
import org.jetbrains.kotlin.idea.debugger.sequence.trace.dsl.KotlinCollectionsPeekCallFactory
import org.jetbrains.kotlin.idea.debugger.sequence.trace.dsl.KotlinStatementFactory
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.KotlinTraceExpressionBuilder
class KotlinCollectionSupportProvider : LibrarySupportProvider {
private companion object {
val builder: StreamChainBuilder = KotlinCollectionChainBuilder()
val support: LibrarySupport = KotlinCollectionLibrarySupport()
val dsl = DslImpl(KotlinStatementFactory(KotlinCollectionsPeekCallFactory()))
}
override fun getLanguageId(): String = KotlinLanguage.INSTANCE.id
override fun getChainBuilder(): StreamChainBuilder = builder
override fun getLibrarySupport(): LibrarySupport = support
override fun getExpressionBuilder(project: Project): TraceExpressionBuilder =
KotlinTraceExpressionBuilder(dsl, support.createHandlerFactory(dsl))
}
@@ -0,0 +1,43 @@
/*
* Copyright 2000-2017 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.idea.debugger.sequence.lib.java
import com.intellij.debugger.streams.lib.LibrarySupport
import com.intellij.debugger.streams.lib.LibrarySupportProvider
import com.intellij.debugger.streams.lib.impl.StandardLibrarySupport
import com.intellij.debugger.streams.trace.TraceExpressionBuilder
import com.intellij.debugger.streams.trace.dsl.impl.DslImpl
import com.intellij.debugger.streams.wrapper.StreamChainBuilder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.debugger.sequence.psi.impl.KotlinChainTransformerImpl
import org.jetbrains.kotlin.idea.debugger.sequence.psi.impl.PackageBasedCallChecker
import org.jetbrains.kotlin.idea.debugger.sequence.psi.impl.TerminatedChainBuilder
import org.jetbrains.kotlin.idea.debugger.sequence.psi.java.JavaStreamChainTypeExtractor
import org.jetbrains.kotlin.idea.debugger.sequence.psi.java.StandardLibraryCallChecker
import org.jetbrains.kotlin.idea.debugger.sequence.trace.dsl.JavaPeekCallFactory
import org.jetbrains.kotlin.idea.debugger.sequence.trace.dsl.KotlinStatementFactory
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.KotlinTraceExpressionBuilder
class JavaStandardLibrarySupportProvider : LibrarySupportProvider {
private companion object {
val builder = TerminatedChainBuilder(
KotlinChainTransformerImpl(JavaStreamChainTypeExtractor()),
StandardLibraryCallChecker(PackageBasedCallChecker("java.util.stream"))
)
val support = StandardLibrarySupport()
val dsl = DslImpl(KotlinStatementFactory(JavaPeekCallFactory()))
}
override fun getLanguageId(): String = KotlinLanguage.INSTANCE.id
override fun getChainBuilder(): StreamChainBuilder = builder
override fun getLibrarySupport(): LibrarySupport = support
override fun getExpressionBuilder(project: Project): TraceExpressionBuilder =
KotlinTraceExpressionBuilder(dsl, support.createHandlerFactory(dsl))
}
@@ -0,0 +1,43 @@
/*
* Copyright 2000-2017 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.idea.debugger.sequence.lib.java
import com.intellij.debugger.streams.lib.LibrarySupport
import com.intellij.debugger.streams.lib.LibrarySupportProvider
import com.intellij.debugger.streams.lib.impl.StreamExLibrarySupport
import com.intellij.debugger.streams.trace.TraceExpressionBuilder
import com.intellij.debugger.streams.trace.dsl.impl.DslImpl
import com.intellij.debugger.streams.wrapper.StreamChainBuilder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.debugger.sequence.psi.impl.KotlinChainTransformerImpl
import org.jetbrains.kotlin.idea.debugger.sequence.psi.impl.PackageBasedCallChecker
import org.jetbrains.kotlin.idea.debugger.sequence.psi.impl.TerminatedChainBuilder
import org.jetbrains.kotlin.idea.debugger.sequence.psi.java.JavaStreamChainTypeExtractor
import org.jetbrains.kotlin.idea.debugger.sequence.psi.java.StreamExCallChecker
import org.jetbrains.kotlin.idea.debugger.sequence.trace.dsl.JavaPeekCallFactory
import org.jetbrains.kotlin.idea.debugger.sequence.trace.dsl.KotlinStatementFactory
import org.jetbrains.kotlin.idea.debugger.sequence.trace.impl.KotlinTraceExpressionBuilder
class StreamExLibrarySupportProvider : LibrarySupportProvider {
private companion object {
val streamChainBuilder = TerminatedChainBuilder(
KotlinChainTransformerImpl(JavaStreamChainTypeExtractor()),
StreamExCallChecker(PackageBasedCallChecker("one.util.streamex"))
)
val support = StreamExLibrarySupport()
val dsl = DslImpl(KotlinStatementFactory(JavaPeekCallFactory()))
val expressionBuilder = KotlinTraceExpressionBuilder(dsl, support.createHandlerFactory(dsl))
}
override fun getLanguageId(): String = KotlinLanguage.INSTANCE.id
override fun getChainBuilder(): StreamChainBuilder = streamChainBuilder
override fun getLibrarySupport(): LibrarySupport = support
override fun getExpressionBuilder(project: Project): TraceExpressionBuilder = expressionBuilder
}

Some files were not shown because too many files have changed in this diff Show More