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
@@ -0,0 +1,22 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
compile(project(":compiler:backend"))
compile(project(":idea:idea-core"))
testCompile(project(":kotlin-test:kotlin-test-junit"))
// TODO: get rid of this
compile(project(":idea:jvm-debugger:eval4j"))
compile(files("${System.getProperty("java.home")}/../lib/tools.jar"))
testCompile(commonDep("junit:junit"))
}
sourceSets {
"main" { projectDefault() }
"test" { none() }
}
@@ -0,0 +1,50 @@
/*
* 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
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
fun getContextElement(elementAt: PsiElement?): PsiElement? {
if (elementAt == null) return null
if (elementAt is PsiCodeBlock) {
return getContextElement(elementAt.context?.context)
}
if (elementAt is KtLightClass) {
return getContextElement(elementAt.kotlinOrigin)
}
val containingFile = elementAt.containingFile
if (containingFile is PsiJavaFile) return elementAt
if (containingFile !is KtFile) return null
// elementAt can be PsiWhiteSpace when codeFragment is created from line start offset (in case of first opening EE window)
val lineStartOffset = if (elementAt is PsiWhiteSpace || elementAt is PsiComment) {
PsiTreeUtil.skipSiblingsForward(elementAt, PsiWhiteSpace::class.java, PsiComment::class.java)?.textOffset
?: elementAt.textOffset
} else {
elementAt.textOffset
}
fun KtElement.takeIfAcceptedAsCodeFragmentContext() = takeIf { KotlinEditorTextProvider.isAcceptedAsCodeFragmentContext(it) }
PsiTreeUtil.findElementOfClassAtOffset(containingFile, lineStartOffset, KtExpression::class.java, false)
?.takeIfAcceptedAsCodeFragmentContext()
?.let { return CodeInsightUtils.getTopmostElementAtOffset(it, lineStartOffset, KtExpression::class.java) }
KotlinEditorTextProvider.findExpressionInner(elementAt, true)
?.takeIfAcceptedAsCodeFragmentContext()
?.let { return it }
return containingFile
}
@@ -0,0 +1,16 @@
/*
* 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
import kotlin.coroutines.Continuation
import org.jetbrains.org.objectweb.asm.Type as AsmType
val CONTINUATION_TYPE: AsmType = AsmType.getType(Continuation::class.java)
val SUSPEND_LAMBDA_CLASSES: List<String> = listOf(
"kotlin.coroutines.jvm.internal.SuspendLambda",
"kotlin.coroutines.jvm.internal.RestrictedSuspendLambda"
)
@@ -0,0 +1,255 @@
/*
* 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.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.Location
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.asJava.finder.JavaElementFinder
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.idea.core.KotlinFileTypeFactory
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.resolve.ResolutionFacade
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil.findFilesWithExactPackage
import org.jetbrains.kotlin.idea.stubindex.StaticFacadeIndexUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.CompositeBindingContext
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedSimpleFunctionDescriptor
import org.jetbrains.kotlin.utils.addIfNotNull
import java.util.*
object DebuggerUtils {
@get:TestOnly
var forceRanking = false
fun findSourceFileForClassIncludeLibrarySources(
project: Project,
scope: GlobalSearchScope,
className: JvmClassName,
fileName: String,
location: Location? = null
): KtFile? {
return runReadAction {
findSourceFileForClass(
project,
listOf(scope, KotlinSourceFilterScope.librarySources(GlobalSearchScope.allScope(project), project)),
className,
fileName,
location
)
}
}
fun findSourceFileForClass(
project: Project,
scopes: List<GlobalSearchScope>,
className: JvmClassName,
fileName: String,
location: Location?
): KtFile? {
if (!isKotlinSourceFile(fileName)) return null
if (DumbService.getInstance(project).isDumb) return null
val partFqName = className.fqNameForClassNameWithoutDollars
for (scope in scopes) {
val files = findFilesByNameInPackage(className, fileName, project, scope)
if (files.isEmpty()) {
continue
}
if (files.size == 1 && !forceRanking || location == null) {
return files.first()
}
StaticFacadeIndexUtil.findFilesForFilePart(partFqName, scope, project)
.singleOrNull { it.name == fileName }
?.let { return it }
return FileRankingCalculatorForIde.findMostAppropriateSource(files, location)
}
return null
}
private fun findFilesByNameInPackage(
className: JvmClassName,
fileName: String,
project: Project,
searchScope: GlobalSearchScope
): List<KtFile> {
val files = findFilesWithExactPackage(className.packageFqName, searchScope, project).filter { it.name == fileName }
return files.sortedWith(JavaElementFinder.byClasspathComparator(searchScope))
}
fun analyzeInlinedFunctions(
resolutionFacadeForFile: ResolutionFacade,
file: KtFile,
analyzeOnlyReifiedInlineFunctions: Boolean,
bindingContext: BindingContext? = null
): Pair<BindingContext, List<KtFile>> {
val analyzedElements = HashSet<KtElement>()
val context = analyzeElementWithInline(
resolutionFacadeForFile,
file,
1,
analyzedElements,
!analyzeOnlyReifiedInlineFunctions, bindingContext
)
//We processing another files just to annotate anonymous classes within their inline functions
//Bytecode not produced for them cause of filtering via generateClassFilter
val toProcess = LinkedHashSet<KtFile>()
toProcess.add(file)
for (collectedElement in analyzedElements) {
val containingFile = collectedElement.containingKtFile
toProcess.add(containingFile)
}
return Pair<BindingContext, List<KtFile>>(context, ArrayList(toProcess))
}
fun analyzeElementWithInline(function: KtNamedFunction, analyzeInlineFunctions: Boolean): Collection<KtElement> {
val analyzedElements = HashSet<KtElement>()
analyzeElementWithInline(function.getResolutionFacade(), function, 1, analyzedElements, !analyzeInlineFunctions)
return analyzedElements
}
fun isKotlinSourceFile(fileName: String): Boolean {
val extension = FileUtilRt.getExtension(fileName).toLowerCase()
return extension in KotlinFileTypeFactory.KOTLIN_EXTENSIONS
}
private fun analyzeElementWithInline(
resolutionFacade: ResolutionFacade,
element: KtElement,
deep: Int,
analyzedElements: MutableSet<KtElement>,
analyzeInlineFunctions: Boolean,
fullResolveContext: BindingContext? = null
): BindingContext {
val project = element.project
val declarationsWithBody = HashSet<KtDeclarationWithBody>()
val innerContexts = ArrayList<BindingContext>()
innerContexts.addIfNotNull(fullResolveContext)
element.accept(object : KtTreeVisitorVoid() {
override fun visitExpression(expression: KtExpression) {
super.visitExpression(expression)
val bindingContext = resolutionFacade.analyze(expression)
innerContexts.add(bindingContext)
val call = bindingContext.get(BindingContext.CALL, expression) ?: return
val resolvedCall = bindingContext.get(BindingContext.RESOLVED_CALL, call)
checkResolveCall(resolvedCall)
}
override fun visitDestructuringDeclaration(destructuringDeclaration: KtDestructuringDeclaration) {
super.visitDestructuringDeclaration(destructuringDeclaration)
val bindingContext = resolutionFacade.analyze(destructuringDeclaration)
innerContexts.add(bindingContext)
for (entry in destructuringDeclaration.entries) {
val resolvedCall = bindingContext.get(BindingContext.COMPONENT_RESOLVED_CALL, entry)
checkResolveCall(resolvedCall)
}
}
override fun visitForExpression(expression: KtForExpression) {
super.visitForExpression(expression)
val bindingContext = resolutionFacade.analyze(expression)
innerContexts.add(bindingContext)
checkResolveCall(bindingContext.get(BindingContext.LOOP_RANGE_ITERATOR_RESOLVED_CALL, expression.loopRange))
checkResolveCall(bindingContext.get(BindingContext.LOOP_RANGE_HAS_NEXT_RESOLVED_CALL, expression.loopRange))
checkResolveCall(bindingContext.get(BindingContext.LOOP_RANGE_NEXT_RESOLVED_CALL, expression.loopRange))
}
private fun checkResolveCall(resolvedCall: ResolvedCall<*>?) {
if (resolvedCall == null) return
val descriptor = resolvedCall.resultingDescriptor
if (descriptor is DeserializedSimpleFunctionDescriptor) return
isAdditionalResolveNeededForDescriptor(descriptor)
if (descriptor is PropertyDescriptor) {
for (accessor in descriptor.accessors) {
isAdditionalResolveNeededForDescriptor(accessor)
}
}
}
private fun isAdditionalResolveNeededForDescriptor(descriptor: CallableDescriptor) {
if (!(InlineUtil.isInline(descriptor) && (analyzeInlineFunctions || hasReifiedTypeParameters(descriptor)))) {
return
}
val declaration = DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor)
if (declaration != null && declaration is KtDeclarationWithBody && !analyzedElements.contains(declaration)) {
declarationsWithBody.add(declaration)
return
}
}
})
analyzedElements.add(element)
if (!declarationsWithBody.isEmpty() && deep < 10) {
for (inlineFunction in declarationsWithBody) {
val body = inlineFunction.bodyExpression
if (body != null) {
innerContexts.add(
analyzeElementWithInline(
resolutionFacade,
inlineFunction,
deep + 1,
analyzedElements,
analyzeInlineFunctions
)
)
}
}
analyzedElements.addAll(declarationsWithBody)
}
return CompositeBindingContext.create(innerContexts)
}
private fun hasReifiedTypeParameters(descriptor: CallableDescriptor): Boolean {
return descriptor.typeParameters.any { it.isReified }
}
}
@@ -0,0 +1,359 @@
/*
* 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.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.psi.PsiElement
import com.sun.jdi.*
import org.jetbrains.kotlin.codegen.ClassBuilderMode
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.diagnostics.DiagnosticUtils
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.debugger.FileRankingCalculator.Ranking.Companion.LOW
import org.jetbrains.kotlin.idea.debugger.FileRankingCalculator.Ranking.Companion.MAJOR
import org.jetbrains.kotlin.idea.debugger.FileRankingCalculator.Ranking.Companion.MINOR
import org.jetbrains.kotlin.idea.debugger.FileRankingCalculator.Ranking.Companion.NORMAL
import org.jetbrains.kotlin.idea.debugger.FileRankingCalculator.Ranking.Companion.ZERO
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypes2
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypes3
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.descriptorUtil.varargParameterPosition
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.keysToMap
import kotlin.jvm.internal.FunctionBase
import org.jetbrains.org.objectweb.asm.Type as AsmType
object FileRankingCalculatorForIde : FileRankingCalculator() {
override fun analyze(element: KtElement) = element.analyze(BodyResolveMode.PARTIAL)
}
abstract class FileRankingCalculator(private val checkClassFqName: Boolean = true) {
abstract fun analyze(element: KtElement): BindingContext
fun findMostAppropriateSource(files: Collection<KtFile>, location: Location): KtFile {
val fileWithRankings: Map<KtFile, Int> = rankFiles(files, location)
val fileWithMaxScore = fileWithRankings.maxBy { it.value }!!
return fileWithMaxScore.key
}
fun rankFiles(files: Collection<KtFile>, location: Location): Map<KtFile, Int> {
assert(files.isNotEmpty())
return files.keysToMap { fileRankingSafe(it, location).value }
}
private class Ranking(val value: Int) : Comparable<Ranking> {
companion object {
val LOW = Ranking(-1000)
val ZERO = Ranking(0)
val MINOR = Ranking(1)
val NORMAL = Ranking(5)
val MAJOR = Ranking(10)
fun minor(condition: Boolean) = if (condition) MINOR else ZERO
}
operator fun unaryMinus() = Ranking(-value)
operator fun plus(other: Ranking) = Ranking(value + other.value)
override fun compareTo(other: Ranking) = this.value - other.value
override fun toString() = value.toString()
}
private fun collect(vararg conditions: Any): Ranking {
return conditions
.map { condition ->
when (condition) {
is Boolean -> Ranking.minor(condition)
is Int -> Ranking(condition)
is Ranking -> condition
else -> error("Invalid condition type ${condition.javaClass.name}")
}
}.fold(ZERO) { sum, r -> sum + r }
}
private fun rankingForClass(clazz: KtClassOrObject, fqName: String, virtualMachine: VirtualMachine): Ranking {
val bindingContext = analyze(clazz)
val descriptor = bindingContext[BindingContext.CLASS, clazz] ?: return ZERO
val jdiType = virtualMachine.classesByName(fqName).firstOrNull() ?: run {
// Check at least the class name if not found
return rankingForClassName(fqName, descriptor, bindingContext)
}
return rankingForClass(clazz, jdiType)
}
private fun rankingForClass(clazz: KtClassOrObject, type: ReferenceType): Ranking {
val bindingContext = analyze(clazz)
val descriptor = bindingContext[BindingContext.CLASS, clazz] ?: return ZERO
return collect(
rankingForClassName(type.name(), descriptor, bindingContext),
Ranking.minor(type.isAbstract && descriptor.modality == Modality.ABSTRACT),
Ranking.minor(type.isFinal && descriptor.modality == Modality.FINAL),
Ranking.minor(type.isStatic && !descriptor.isInner),
rankingForVisibility(descriptor, type)
)
}
private fun rankingForClassName(fqName: String, descriptor: ClassDescriptor, bindingContext: BindingContext): Ranking {
if (DescriptorUtils.isLocal(descriptor)) return Ranking.ZERO
val expectedFqName = makeTypeMapper(bindingContext).mapType(descriptor).className
return when {
checkClassFqName -> if (expectedFqName == fqName) MAJOR else LOW
else -> if (expectedFqName.simpleName() == fqName.simpleName()) MAJOR else LOW
}
}
private fun rankingForMethod(function: KtFunction, method: Method): Ranking {
val bindingContext = analyze(function)
val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, function] as? CallableMemberDescriptor ?: return ZERO
if (function !is KtConstructor<*> && method.name() != descriptor.name.asString())
return LOW
return collect(
method.isConstructor && function is KtConstructor<*>,
method.isAbstract && descriptor.modality == Modality.ABSTRACT,
method.isFinal && descriptor.modality == Modality.FINAL,
method.isVarArgs && descriptor.varargParameterPosition() >= 0,
rankingForVisibility(descriptor, method),
descriptor.valueParameters.size == (method.safeArguments()?.size ?: 0)
)
}
private fun rankingForAccessor(accessor: KtPropertyAccessor, method: Method): Ranking {
val methodName = method.name()
val expectedPropertyName = accessor.property.name ?: return ZERO
if (accessor.isSetter) {
if (!methodName.startsWith("set") || method.returnType() !is VoidType || method.argumentTypes().size != 1)
return -MAJOR
}
if (accessor.isGetter) {
if (!methodName.startsWith("get") && !methodName.startsWith("is"))
return -MAJOR
else if (method.returnType() is VoidType || method.argumentTypes().isNotEmpty())
return -NORMAL
}
val actualPropertyName = getPropertyName(methodName, accessor.isSetter)
return if (expectedPropertyName == actualPropertyName) NORMAL else -NORMAL
}
private fun getPropertyName(accessorMethodName: String, isSetter: Boolean): String {
if (isSetter) {
return accessorMethodName.drop(3)
}
return accessorMethodName.drop(if (accessorMethodName.startsWith("is")) 2 else 3)
}
private fun rankingForProperty(property: KtProperty, method: Method): Ranking {
val methodName = method.name()
val propertyName = property.name ?: return ZERO
if (property.isTopLevel && method.name() == "<clinit>") {
// For top-level property initializers
return MINOR
}
if (!methodName.startsWith("get") && !methodName.startsWith("set"))
return -MAJOR
// boolean is
return if (methodName.drop(3) == propertyName.capitalize()) MAJOR else -NORMAL
}
private fun rankingForVisibility(descriptor: DeclarationDescriptorWithVisibility, accessible: Accessible): Ranking {
return collect(
accessible.isPublic && descriptor.visibility == Visibilities.PUBLIC,
accessible.isProtected && descriptor.visibility == Visibilities.PROTECTED,
accessible.isPrivate && descriptor.visibility == Visibilities.PRIVATE
)
}
private fun fileRankingSafe(file: KtFile, location: Location): Ranking {
return try {
fileRanking(file, location)
} catch (e: ClassNotLoadedException) {
LOG.error("ClassNotLoadedException should never happen in FileRankingCalculator", e)
ZERO
} catch (e: AbsentInformationException) {
ZERO
} catch (e: InternalException) {
ZERO
} catch (e: ProcessCanceledException) {
throw e
} catch (e: RuntimeException) {
LOG.error("Exception during Kotlin sources ranking", e)
ZERO
}
}
private fun fileRanking(file: KtFile, location: Location): Ranking {
val locationLineNumber = location.lineNumber() - 1
val lineStartOffset = file.getLineStartOffset(locationLineNumber) ?: return LOW
val elementAt = file.findElementAt(lineStartOffset) ?: return ZERO
var overallRanking = ZERO
val method = location.method()
if (method.isLambda()) {
val (className, methodName) = method.getContainingClassAndMethodNameForLambda() ?: return ZERO
if (method.isBridge && method.isSynthetic) {
// It might be a static lambda field accessor
val containingClass = elementAt.getParentOfType<KtClassOrObject>(false) ?: return LOW
return rankingForClass(containingClass, className, location.virtualMachine())
} else {
val containingFunctionLiteral = findFunctionLiteralOnLine(elementAt) ?: return LOW
val containingCallable = findNonLocalCallableParent(containingFunctionLiteral) ?: return LOW
when (containingCallable) {
is KtFunction -> if (containingCallable.name == methodName) overallRanking += MAJOR
is KtProperty -> if (containingCallable.name == methodName) overallRanking += MAJOR
is KtPropertyAccessor -> if (containingCallable.property.name == methodName) overallRanking += MAJOR
}
val containingClass = containingCallable.getParentOfType<KtClassOrObject>(false)
if (containingClass != null) {
overallRanking += rankingForClass(containingClass, className, location.virtualMachine())
}
return overallRanking
}
}
// TODO support <clinit>
if (method.name() == "<init>") {
val containingClass = elementAt.getParentOfType<KtClassOrObject>(false) ?: return LOW
val constructorOrInitializer =
elementAt.getParentOfTypes2<KtConstructor<*>, KtClassInitializer>()?.takeIf { containingClass.isAncestor(it) }
?: containingClass.primaryConstructor?.takeIf { it.getLine() == containingClass.getLine() }
if (constructorOrInitializer == null
&& locationLineNumber < containingClass.getLine()
&& locationLineNumber > containingClass.lastChild.getLine()
) {
return LOW
}
overallRanking += rankingForClass(containingClass, location.declaringType())
if (constructorOrInitializer is KtConstructor<*>)
overallRanking += rankingForMethod(constructorOrInitializer, method)
} else {
val callable = findNonLocalCallableParent(elementAt) ?: return LOW
overallRanking += when (callable) {
is KtFunction -> rankingForMethod(callable, method)
is KtPropertyAccessor -> rankingForAccessor(callable, method)
is KtProperty -> rankingForProperty(callable, method)
else -> return LOW
}
val containingClass = elementAt.getParentOfType<KtClassOrObject>(false)
if (containingClass != null)
overallRanking += rankingForClass(containingClass, location.declaringType())
}
return overallRanking
}
private fun findFunctionLiteralOnLine(element: PsiElement): KtFunctionLiteral? {
val literal = element.getParentOfType<KtFunctionLiteral>(false)
if (literal != null) {
return literal
}
val callExpression = element.getParentOfType<KtCallExpression>(false) ?: return null
for (lambdaArgument in callExpression.lambdaArguments) {
if (element.getLine() == lambdaArgument.getLine()) {
val functionLiteral = lambdaArgument.getLambdaExpression()?.functionLiteral
if (functionLiteral != null) {
return functionLiteral
}
}
}
return null
}
private tailrec fun findNonLocalCallableParent(element: PsiElement): PsiElement? {
fun PsiElement.isCallableDeclaration() = this is KtProperty || this is KtFunction || this is KtAnonymousInitializer
// org.jetbrains.kotlin.psi.KtPsiUtil.isLocal
fun PsiElement.isLocalDeclaration(): Boolean {
val containingDeclaration = getParentOfType<KtDeclaration>(true)
return containingDeclaration is KtCallableDeclaration || containingDeclaration is KtPropertyAccessor
}
if (element.isCallableDeclaration() && !element.isLocalDeclaration()) {
return element
}
val containingCallable = element.getParentOfTypes3<KtProperty, KtFunction, KtAnonymousInitializer>()
?: return null
if (containingCallable.isLocalDeclaration()) {
return findNonLocalCallableParent(containingCallable)
}
return containingCallable
}
private fun Method.getContainingClassAndMethodNameForLambda(): Pair<String, String>? {
// TODO this breaks nested classes
val declaringClass = declaringType() as ClassType
val (className, methodName) = declaringClass.name().split('$', limit = 3)
.takeIf { it.size == 3 }
?: return null
return Pair(className, methodName)
}
private fun Method.isLambda(): Boolean {
val declaringClass = declaringType() as? ClassType ?: return false
tailrec fun ClassType.isLambdaClass(): Boolean {
if (interfaces().any { it.name() == FunctionBase::class.java.name }) {
return true
}
val superClass = superclass() ?: return false
return superClass.isLambdaClass()
}
return declaringClass.superclass().isLambdaClass()
}
private fun makeTypeMapper(bindingContext: BindingContext): KotlinTypeMapper {
return KotlinTypeMapper(
bindingContext,
ClassBuilderMode.LIGHT_CLASSES,
"debugger",
KotlinTypeMapper.LANGUAGE_VERSION_SETTINGS_DEFAULT // TODO use proper LanguageVersionSettings
)
}
companion object {
val LOG = Logger.getInstance("FileRankingCalculator")
}
}
private fun String.simpleName() = substringAfterLast('.').substringAfterLast('$')
private fun PsiElement.getLine(): Int {
return DiagnosticUtils.getLineAndColumnInPsiFile(containingFile, textRange).line
}
@@ -0,0 +1,52 @@
/*
* 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
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.load.java.JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT
import org.jetbrains.kotlin.load.java.JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION
val INLINED_THIS_REGEX = getLocalVariableNameRegexInlineAware(AsmUtil.INLINE_DECLARATION_SITE_THIS)
fun getInlineDepth(variables: List<LocalVariableProxyImpl>): Int {
val inlineFunVariables = variables
.filter { it.name().startsWith(LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION) }
if (inlineFunVariables.isEmpty()) {
return 0
}
val closestInlineFun = inlineFunVariables.maxBy { it.variable }!!.variable
val inlineLambdaDepth = variables
.count { it.name().startsWith(LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT) && it.variable > closestInlineFun }
return maxOf(0, inlineFunVariables.size - inlineLambdaDepth)
}
fun getInlineDepth(variableName: String): Int {
var endIndex = variableName.length
var depth = 0
val suffixLen = INLINE_FUN_VAR_SUFFIX.length
while (endIndex >= suffixLen) {
if (variableName.substring(endIndex - suffixLen, endIndex) != INLINE_FUN_VAR_SUFFIX) {
break
}
depth++
endIndex -= suffixLen
}
return depth
}
private fun getLocalVariableNameRegexInlineAware(name: String): Regex {
val escapedName = Regex.escape(name)
val escapedSuffix = Regex.escape(INLINE_FUN_VAR_SUFFIX)
return Regex("^$escapedName(?:$escapedSuffix)*$")
}
@@ -0,0 +1,123 @@
/*
* 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.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.evaluation.TextWithImportsImpl
import com.intellij.debugger.impl.EditorTextProvider
import com.intellij.openapi.util.Pair
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
class KotlinEditorTextProvider : EditorTextProvider {
override fun getEditorText(elementAtCaret: PsiElement): TextWithImports? {
val expression = findExpressionInner(elementAtCaret, true) ?: return null
val expressionText = getElementInfo(expression) { it.text }
return TextWithImportsImpl(CodeFragmentKind.EXPRESSION, expressionText, "", KotlinFileType.INSTANCE)
}
override fun findExpression(elementAtCaret: PsiElement, allowMethodCalls: Boolean): Pair<PsiElement, TextRange>? {
val expression = findExpressionInner(elementAtCaret, allowMethodCalls) ?: return null
val expressionRange = getElementInfo(expression) { it.textRange }
return Pair(expression, expressionRange)
}
companion object {
fun <T> getElementInfo(expr: KtExpression, f: (PsiElement) -> T): T {
var expressionText = f(expr)
if (expr is KtProperty) {
val nameIdentifier = expr.nameIdentifier
if (nameIdentifier != null) {
expressionText = f(nameIdentifier)
}
}
return expressionText
}
fun findExpressionInner(element: PsiElement, allowMethodCalls: Boolean): KtExpression? {
if (!isAcceptedAsCodeFragmentContext(element)) return null
val ktElement = PsiTreeUtil.getParentOfType(element, KtElement::class.java) ?: return null
if (ktElement is KtProperty) {
val nameIdentifier = ktElement.nameIdentifier
if (nameIdentifier == element) {
return ktElement
}
}
fun KtExpression.qualifiedParentOrSelf(isSelector: Boolean = true): KtExpression {
val parent = parent
return if (parent is KtQualifiedExpression && (!isSelector || parent.selectorExpression == this)) parent else this
}
val parent = ktElement.parent
val newExpression = when (parent) {
is KtThisExpression -> parent
is KtSuperExpression -> parent.qualifiedParentOrSelf(isSelector = false)
is KtArrayAccessExpression -> if (parent.arrayExpression == ktElement) ktElement else parent.qualifiedParentOrSelf()
is KtReferenceExpression -> parent.qualifiedParentOrSelf()
is KtQualifiedExpression -> if (parent.receiverExpression != ktElement) parent else null
is KtOperationExpression -> if (parent.operationReference == ktElement) parent else null
else -> null
}
if (!allowMethodCalls && newExpression != null) {
fun PsiElement.isCall() = this is KtCallExpression || this is KtOperationExpression || this is KtArrayAccessExpression
if (newExpression.isCall() || newExpression is KtQualifiedExpression && newExpression.selectorExpression!!.isCall()) {
return null
}
}
return when {
newExpression is KtExpression -> newExpression
ktElement is KtSimpleNameExpression -> {
val context = ktElement.analyze()
val qualifier = context[BindingContext.QUALIFIER, ktElement]
if (qualifier != null && !DescriptorUtils.isObject(qualifier.descriptor)) {
null
}
else {
ktElement
}
}
else -> null
}
}
private val NOT_ACCEPTED_AS_CONTEXT_TYPES =
arrayOf(KtUserType::class.java, KtImportDirective::class.java, KtPackageDirective::class.java, KtValueArgumentName::class.java)
fun isAcceptedAsCodeFragmentContext(element: PsiElement): Boolean {
return !NOT_ACCEPTED_AS_CONTEXT_TYPES.contains(element::class.java as Class<*>) &&
PsiTreeUtil.getParentOfType(element, *NOT_ACCEPTED_AS_CONTEXT_TYPES) == null
}
}
}
@@ -0,0 +1,338 @@
/*
* 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.SourcePosition
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.compiler.CompilerPaths
import com.intellij.openapi.compiler.ex.CompilerPathsEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.containers.ConcurrentFactoryMap
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import com.sun.jdi.VirtualMachine
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches
import org.jetbrains.kotlin.idea.core.util.getLineCount
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.idea.core.util.toPsiFile
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.tail
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.getOrPutNullable
import org.jetbrains.org.objectweb.asm.*
import java.io.File
import java.util.*
import java.util.concurrent.ConcurrentMap
fun isInlineFunctionLineNumber(file: VirtualFile, lineNumber: Int, project: Project): Boolean {
if (ProjectRootsUtil.isProjectSourceFile(project, file)) {
val linesInFile = file.toPsiFile(project)?.getLineCount() ?: return false
return lineNumber > linesInFile
}
return true
}
fun readBytecodeInfo(project: Project,
jvmName: JvmClassName,
file: VirtualFile): BytecodeDebugInfo? {
return KotlinDebuggerCaches.getOrReadDebugInfoFromBytecode(project, jvmName, file)
}
fun ktLocationInfo(location: Location, isDexDebug: Boolean, project: Project,
preferInlined: Boolean = false, locationFile: KtFile? = null): Pair<Int, KtFile?> {
if (isDexDebug && (locationFile == null || location.lineNumber() > locationFile.getLineCount())) {
if (!preferInlined) {
val thisFunLine = runReadAction { getLastLineNumberForLocation(location, project) }
if (thisFunLine != null && thisFunLine != location.lineNumber()) {
return thisFunLine to locationFile
}
}
val inlinePosition = runReadAction { getOriginalPositionOfInlinedLine(location, project) }
if (inlinePosition != null) {
val (file, line) = inlinePosition
return line + 1 to file
}
}
return location.lineNumber() to locationFile
}
/**
* Only the first line number is stored for instruction in dex. It can be obtained through location.lineNumber().
* This method allows to get last stored linenumber for instruction.
*/
fun getLastLineNumberForLocation(location: Location, project: Project, searchScope: GlobalSearchScope = GlobalSearchScope.allScope(project)): Int? {
val lineNumber = location.lineNumber()
val fqName = FqName(location.declaringType().name())
val fileName = location.sourceName()
val method = location.method() ?: return null
val name = method.name() ?: return null
val signature = method.signature() ?: return null
val debugInfo = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null
val lineMapping = debugInfo.lineTableMapping[BytecodeMethodKey(name, signature)] ?: return null
return lineMapping.values.firstOrNull { it.contains(lineNumber) }?.last()
}
fun createWeakBytecodeDebugInfoStorage(): ConcurrentMap<BinaryCacheKey, BytecodeDebugInfo?> {
return ConcurrentFactoryMap.createWeakMap<BinaryCacheKey, BytecodeDebugInfo?> { key ->
val bytes = readClassFileImpl(key.project, key.jvmName, key.file) ?: return@createWeakMap null
val smapData = readDebugInfo(bytes)
val lineNumberMapping = readLineNumberTableMapping(bytes)
BytecodeDebugInfo(smapData, lineNumberMapping)
}
}
class BytecodeDebugInfo(val smapData: SmapData?, val lineTableMapping: Map<BytecodeMethodKey, Map<String, Set<Int>>>)
data class BytecodeMethodKey(val methodName: String, val signature: String)
data class BinaryCacheKey(val project: Project, val jvmName: JvmClassName, val file: VirtualFile)
private fun readClassFileImpl(project: Project,
jvmName: JvmClassName,
file: VirtualFile): ByteArray? {
val fqNameWithInners = jvmName.fqNameForClassNameWithoutDollars.tail(jvmName.packageFqName)
fun readFromLibrary(): ByteArray? {
if (!ProjectRootsUtil.isLibrarySourceFile(project, file)) return null
val classId = ClassId(jvmName.packageFqName, Name.identifier(fqNameWithInners.asString()))
// TODO use debugger search scope
val fileFinder = VirtualFileFinderFactory.getInstance(project).create(GlobalSearchScope.allScope(project))
val classFile = fileFinder.findVirtualFileWithHeader(classId) ?: return null
return classFile.contentsToByteArray(false)
}
fun readFromOutput(isForTestClasses: Boolean): ByteArray? {
if (!ProjectRootsUtil.isProjectSourceFile(project, file)) return null
val module = ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(file) ?: return null
val outputPaths = CompilerPathsEx.getOutputPaths(arrayOf(module)).toList()
val className = fqNameWithInners.asString().replace('.', '$')
var classFile = findClassFileByPaths(jvmName.packageFqName.asString(), className, outputPaths)
if (classFile == null) {
if (!isForTestClasses) {
return null
}
val outputDir = CompilerPaths.getModuleOutputDirectory(module, /*forTests = */ isForTestClasses) ?: return null
val outputModeDirName = outputDir.name
// FIXME: It looks like this doesn't work anymore after Kotlin gradle plugin have stopped generating Kotlin classes in java output dir
// Originally this code did mapping like 'path/classes/test/debug' -> 'path/classes/androidTest/debug'
val androidTestOutputDir = outputDir.parent?.parent?.findChild("androidTest")?.findChild(outputModeDirName) ?: return null
classFile = findClassFileByPath(jvmName.packageFqName.asString(), className, androidTestOutputDir.path) ?: return null
}
return classFile.readBytes()
}
fun readFromSourceOutput(): ByteArray? = readFromOutput(false)
fun readFromTestOutput(): ByteArray? = readFromOutput(true)
return readFromLibrary() ?:
readFromSourceOutput() ?:
readFromTestOutput()
}
private fun findClassFileByPaths(packageName: String, className: String, paths: List<String>): File? =
paths.mapNotNull { path -> findClassFileByPath(packageName, className, path) }.maxBy { it.lastModified() }
private fun findClassFileByPath(packageName: String, className: String, outputDirPath: String): File? {
val outDirFile = File(outputDirPath).takeIf(File::exists) ?: return null
val parentDirectory = File(outDirFile, packageName.replace(".", File.separator))
if (!parentDirectory.exists()) return null
if (ApplicationManager.getApplication().isUnitTestMode) {
val beforeDexFileClassFile = File(parentDirectory, className + ".class.before_dex")
if (beforeDexFileClassFile.exists()) {
return beforeDexFileClassFile
}
}
val classFile = File(parentDirectory, className + ".class")
if (classFile.exists()) {
return classFile
}
return null
}
private fun readLineNumberTableMapping(bytes: ByteArray): Map<BytecodeMethodKey, Map<String, Set<Int>>> {
val lineNumberMapping = HashMap<BytecodeMethodKey, Map<String, Set<Int>>>()
ClassReader(bytes).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (name == null || desc == null) {
return null
}
val methodKey = BytecodeMethodKey(name, desc)
val methodLinesMapping = HashMap<String, MutableSet<Int>>()
lineNumberMapping[methodKey] = methodLinesMapping
return object : MethodVisitor(Opcodes.API_VERSION, null) {
override fun visitLineNumber(line: Int, start: Label?) {
if (start != null) {
methodLinesMapping.getOrPutNullable(start.toString(), { LinkedHashSet<Int>() }).add(line)
}
}
}
}
}, ClassReader.SKIP_FRAMES and ClassReader.SKIP_CODE)
return lineNumberMapping
}
internal fun getOriginalPositionOfInlinedLine(location: Location, project: Project): Pair<KtFile, Int>? {
val lineNumber = location.lineNumber()
val fqName = FqName(location.declaringType().name())
val fileName = location.sourceName()
val searchScope = GlobalSearchScope.allScope(project)
val debugInfo = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?:
return null
val smapData = debugInfo.smapData ?: return null
return mapStacktraceLineToSource(smapData, lineNumber, project, SourceLineKind.EXECUTED_LINE, searchScope)
}
private fun findAndReadClassFile(
fqName: FqName, fileName: String, project: Project, searchScope: GlobalSearchScope,
fileFilter: (VirtualFile) -> Boolean): BytecodeDebugInfo? {
val internalName = fqName.asString().replace('.', '/')
val jvmClassName = JvmClassName.byInternalName(internalName)
val file = DebuggerUtils.findSourceFileForClassIncludeLibrarySources(project, searchScope, jvmClassName, fileName) ?: return null
val virtualFile = file.virtualFile ?: return null
if (!fileFilter(virtualFile)) return null
return readBytecodeInfo(project, jvmClassName, virtualFile)
}
fun getLocationsOfInlinedLine(type: ReferenceType, position: SourcePosition, sourceSearchScope: GlobalSearchScope): List<Location> {
val line = position.line
val file = position.file
val project = position.file.project
val lineStartOffset = file.getLineStartOffset(line) ?: return listOf()
val element = file.findElementAt(lineStartOffset) ?: return listOf()
val ktElement = element.parents.firstIsInstanceOrNull<KtElement>() ?: return listOf()
val isInInline = runReadAction { element.parents.any { it is KtFunction && it.hasModifier(KtTokens.INLINE_KEYWORD) } }
if (!isInInline) {
// Lambdas passed to crossinline arguments are inlined when they are used in non-inlined lambdas
val isInCrossinlineArgument = isInCrossinlineArgument(ktElement)
if (!isInCrossinlineArgument) {
return listOf()
}
}
val lines = inlinedLinesNumbers(line + 1, position.file.name, FqName(type.name()), type.sourceName(), project, sourceSearchScope)
return lines.flatMap { type.locationsOfLine(it) }
}
fun isInCrossinlineArgument(ktElement: KtElement): Boolean {
val argumentFunctions = runReadAction {
ktElement.parents.filter {
when (it) {
is KtFunctionLiteral -> it.parent is KtLambdaExpression && (it.parent.parent is KtValueArgument || it.parent.parent is KtLambdaArgument)
is KtFunction -> it.parent is KtValueArgument
else -> false
}
}.filterIsInstance<KtFunction>()
}
val bindingContext = ktElement.analyze(BodyResolveMode.PARTIAL)
return argumentFunctions.any {
val argumentDescriptor = InlineUtil.getInlineArgumentDescriptor(it, bindingContext)
argumentDescriptor?.isCrossinline ?: false
}
}
private fun inlinedLinesNumbers(
inlineLineNumber: Int, inlineFileName: String,
destinationTypeFqName: FqName, destinationFileName: String,
project: Project, sourceSearchScope: GlobalSearchScope): List<Int> {
val internalName = destinationTypeFqName.asString().replace('.', '/')
val jvmClassName = JvmClassName.byInternalName(internalName)
val file = DebuggerUtils.findSourceFileForClassIncludeLibrarySources(project, sourceSearchScope, jvmClassName, destinationFileName) ?:
return listOf()
val virtualFile = file.virtualFile ?: return listOf()
val debugInfo = readBytecodeInfo(project, jvmClassName, virtualFile) ?: return listOf()
val smapData = debugInfo.smapData ?: return listOf()
val smap = smapData.kotlinStrata ?: return listOf()
val mappingsToInlinedFile = smap.fileMappings.filter { it.name == inlineFileName }
val mappingIntervals = mappingsToInlinedFile.flatMap { it.lineMappings }
return mappingIntervals.asSequence().
filter { rangeMapping -> rangeMapping.hasMappingForSource(inlineLineNumber) }.
map { rangeMapping -> rangeMapping.mapSourceToDest(inlineLineNumber) }.
filter { line -> line != -1 }.
toList()
}
@Volatile var emulateDexDebugInTests: Boolean = false
fun DebugProcess.isDexDebug(): Boolean {
val virtualMachine = (this.virtualMachineProxy as? VirtualMachineProxyImpl)?.virtualMachine
return virtualMachine.isDexDebug()
}
fun VirtualMachine?.isDexDebug(): Boolean {
// TODO: check other machine names
return (emulateDexDebugInTests && ApplicationManager.getApplication().isUnitTestMode) || this?.name() == "Dalvik"
}
@@ -0,0 +1,74 @@
/*
* 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
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import com.sun.jdi.ClassType
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.idea.caches.resolve.util.getJavaClassDescriptor
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import com.sun.jdi.Type as JdiType
import org.jetbrains.org.objectweb.asm.Type as AsmType
fun JdiType.isSubtype(className: String): Boolean = isSubtype(AsmType.getObjectType(className))
fun JdiType.isSubtype(type: AsmType): Boolean {
if (this.signature() == type.descriptor) {
return true
}
if (type.sort != AsmType.OBJECT || this !is ClassType) {
return false
}
val superTypeName = type.className
if (allInterfaces().any { it.name() == superTypeName }) {
return true
}
var superClass = superclass()
while (superClass != null) {
if (superClass.name() == superTypeName) {
return true
}
superClass = superClass.superclass()
}
return false
}
fun AsmType.getClassDescriptor(
scope: GlobalSearchScope,
mapBuiltIns: Boolean = true,
moduleDescriptor: ModuleDescriptor = DefaultBuiltIns.Instance.builtInsModule
): ClassDescriptor? {
if (AsmUtil.isPrimitive(this)) return null
val jvmName = JvmClassName.byInternalName(internalName).fqNameForClassNameWithoutDollars
if (mapBuiltIns) {
val mappedName = JavaToKotlinClassMap.mapJavaToKotlin(jvmName)
if (mappedName != null) {
moduleDescriptor.findClassAcrossModuleDependencies(mappedName)?.let { return it }
}
}
return runReadAction {
val classes = JavaPsiFacade.getInstance(scope.project).findClasses(jvmName.asString(), scope)
if (classes.isEmpty()) null
else {
classes.first().getJavaClassDescriptor()
}
}
}
@@ -0,0 +1,97 @@
/*
* 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 com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.SuspendContextImpl
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.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
import com.intellij.openapi.project.Project
import com.sun.jdi.*
import com.sun.jdi.request.EventRequest
import org.jetbrains.org.objectweb.asm.Type
class ExecutionContext(val evaluationContext: EvaluationContextImpl, val frameProxy: StackFrameProxyImpl) {
val vm: VirtualMachineProxyImpl
get() = evaluationContext.debugProcess.virtualMachineProxy
val classLoader: ClassLoaderReference?
get() = evaluationContext.classLoader
val suspendContext: SuspendContextImpl
get() = evaluationContext.suspendContext
val debugProcess: DebugProcessImpl
get() = evaluationContext.debugProcess
val project: Project
get() = evaluationContext.project
val invokePolicy = run {
val suspendContext = evaluationContext.suspendContext
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD) ObjectReference.INVOKE_SINGLE_THREADED else 0
}
@Throws(EvaluateException::class)
fun invokeMethod(obj: ObjectReference, method: Method, args: List<Value?>): Value? {
return debugProcess.invokeInstanceMethod(evaluationContext, obj, method, args, invokePolicy)
}
fun invokeMethod(type: ClassType, method: Method, args: List<Value?>): Value? {
return debugProcess.invokeMethod(evaluationContext, type, method, args)
}
@Throws(EvaluateException::class)
fun newInstance(type: ClassType, constructor: Method, args: List<Value?>): ObjectReference {
return debugProcess.newInstance(evaluationContext, type, constructor, args)
}
@Throws(EvaluateException::class)
fun newInstance(arrayType: ArrayType, dimension: Int): ArrayReference {
return debugProcess.newInstance(arrayType, dimension)
}
@Throws(EvaluateException::class)
fun findClass(name: String, classLoader: ClassLoaderReference? = null): ReferenceType? {
debugProcess.findClass(evaluationContext, name, classLoader)?.let { return it }
// If 'isAutoLoadClasses' is true, `findClass()` already did this
if (!evaluationContext.isAutoLoadClasses) {
try {
debugProcess.loadClass(evaluationContext, name, classLoader)
} catch (e: InvocationException) {
throw EvaluateExceptionUtil.createEvaluateException(e)
} catch (e: ClassNotLoadedException) {
throw EvaluateExceptionUtil.createEvaluateException(e)
} catch (e: IncompatibleThreadStateException) {
throw EvaluateExceptionUtil.createEvaluateException(e)
} catch (e: InvalidTypeException) {
throw EvaluateExceptionUtil.createEvaluateException(e)
}
}
return null
}
@Throws(EvaluateException::class)
fun findClass(asmType: Type, classLoader: ClassLoaderReference? = null): ReferenceType? {
if (asmType.sort != Type.OBJECT && asmType.sort != Type.ARRAY) {
return null
}
return findClass(asmType.className, classLoader)
}
fun keepReference(reference: ObjectReference) {
// Not available in older IDEA versions
@Suppress("DEPRECATION")
DebuggerUtilsEx.keep(reference, evaluationContext)
}
}
@@ -0,0 +1,238 @@
/*
* 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.evaluation.EvaluateException
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.libraries.LibraryUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.containers.MultiMap
import org.jetbrains.annotations.TestOnly
import org.apache.log4j.Logger
import org.jetbrains.eval4j.Value
import org.jetbrains.kotlin.analyzer.AnalysisResult
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.idea.caches.resolve.analyzeAndGetResult
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.idea.core.util.runInReadActionWithWriteActionPriorityWithPCE
import org.jetbrains.kotlin.idea.debugger.BinaryCacheKey
import org.jetbrains.kotlin.idea.debugger.BytecodeDebugInfo
import org.jetbrains.kotlin.idea.debugger.createWeakBytecodeDebugInfoStorage
import org.jetbrains.kotlin.idea.debugger.evaluate.compilation.CompiledDataDescriptor
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.types.KotlinType
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class KotlinDebuggerCaches(project: Project) {
private val cachedCompiledData = CachedValuesManager.getManager(project).createCachedValue(
{
CachedValueProvider.Result<MultiMap<String, CompiledDataDescriptor>>(
MultiMap.create(), PsiModificationTracker.MODIFICATION_COUNT
)
}, false
)
private val cachedClassNames = CachedValuesManager.getManager(project).createCachedValue(
{
CachedValueProvider.Result<MutableMap<PsiElement, List<String>>>(
ConcurrentHashMap(),
PsiModificationTracker.MODIFICATION_COUNT
)
}, false
)
private val cachedTypeMappers = CachedValuesManager.getManager(project).createCachedValue(
{
CachedValueProvider.Result<MutableMap<PsiElement, KotlinTypeMapper>>(
ConcurrentHashMap(),
PsiModificationTracker.MODIFICATION_COUNT
)
}, false
)
private val debugInfoCache = CachedValuesManager.getManager(project).createCachedValue(
{
CachedValueProvider.Result(
createWeakBytecodeDebugInfoStorage(),
PsiModificationTracker.MODIFICATION_COUNT
)
}, false
)
companion object {
private val LOG = Logger.getLogger(KotlinDebuggerCaches::class.java)!!
fun getInstance(project: Project) = ServiceManager.getService(project, KotlinDebuggerCaches::class.java)!!
fun compileCodeFragmentCacheAware(
codeFragment: KtCodeFragment,
sourcePosition: SourcePosition?,
compileCode: () -> CompiledDataDescriptor,
force: Boolean = false
): Pair<CompiledDataDescriptor, Boolean> {
if (sourcePosition == null) {
return Pair(compileCode(), false)
}
val evaluateExpressionCache = getInstance(codeFragment.project)
val text = "${codeFragment.importsToString()}\n${codeFragment.text}"
val cachedResults = synchronized<Collection<CompiledDataDescriptor>>(evaluateExpressionCache.cachedCompiledData) {
evaluateExpressionCache.cachedCompiledData.value[text]
}
val existingResult = cachedResults.firstOrNull { it.sourcePosition == sourcePosition }
if (existingResult != null) {
if (force) {
synchronized(evaluateExpressionCache.cachedCompiledData) {
evaluateExpressionCache.cachedCompiledData.value.remove(text, existingResult)
}
} else {
return Pair(existingResult, true)
}
}
val newCompiledData = compileCode()
LOG.debug("Compile bytecode for ${codeFragment.text}")
synchronized(evaluateExpressionCache.cachedCompiledData) {
evaluateExpressionCache.cachedCompiledData.value.putValue(text, newCompiledData)
}
return Pair(newCompiledData, false)
}
fun <T : PsiElement> getOrComputeClassNames(psiElement: T?, create: (T) -> ComputedClassNames): List<String> {
if (psiElement == null) return Collections.emptyList()
val cache = getInstance(runReadAction { psiElement.project })
val classNamesCache = cache.cachedClassNames.value
val cachedValue = classNamesCache[psiElement]
if (cachedValue != null) return cachedValue
val computedClassNames = create(psiElement)
if (computedClassNames.shouldBeCached) {
classNamesCache[psiElement] = computedClassNames.classNames
}
return computedClassNames.classNames
}
fun getOrCreateTypeMapper(psiElement: PsiElement): KotlinTypeMapper {
val cache = getInstance(runReadAction { psiElement.project })
val file = runReadAction { psiElement.containingFile as KtFile }
val isInLibrary = runReadAction { LibraryUtil.findLibraryEntry(file.virtualFile, file.project) } != null
val key = if (!isInLibrary) file else psiElement
val typeMappersCache = cache.cachedTypeMappers.value
val cachedValue = typeMappersCache[key]
if (cachedValue != null) return cachedValue
val newValue = if (!isInLibrary) {
createTypeMapperForSourceFile(file)
} else {
val element = getElementToCreateTypeMapperForLibraryFile(psiElement)
createTypeMapperForLibraryFile(element, file)
}
typeMappersCache[key] = newValue
return newValue
}
fun getOrReadDebugInfoFromBytecode(
project: Project,
jvmName: JvmClassName,
file: VirtualFile
): BytecodeDebugInfo? {
val cache = getInstance(project)
return cache.debugInfoCache.value[BinaryCacheKey(project, jvmName, file)]
}
private fun getElementToCreateTypeMapperForLibraryFile(element: PsiElement?) =
runReadAction { element as? KtElement ?: PsiTreeUtil.getParentOfType(element, KtElement::class.java)!! }
private fun createTypeMapperForLibraryFile(element: KtElement, file: KtFile): KotlinTypeMapper =
runInReadActionWithWriteActionPriorityWithPCE {
createTypeMapper(file, element.analyzeAndGetResult())
}
private fun createTypeMapperForSourceFile(file: KtFile): KotlinTypeMapper =
runInReadActionWithWriteActionPriorityWithPCE {
createTypeMapper(file, file.analyzeWithAllCompilerChecks().apply(AnalysisResult::throwIfError))
}
private fun createTypeMapper(file: KtFile, analysisResult: AnalysisResult): KotlinTypeMapper {
val state = GenerationState.Builder(
file.project,
ClassBuilderFactories.THROW_EXCEPTION,
analysisResult.moduleDescriptor,
analysisResult.bindingContext,
listOf(file),
CompilerConfiguration.EMPTY
).build()
state.beforeCompile()
return state.typeMapper
}
@TestOnly
fun addTypeMapper(file: KtFile, typeMapper: KotlinTypeMapper) {
getInstance(file.project).cachedTypeMappers.value[file] = typeMapper
}
}
data class Parameter(val callText: String, val type: KotlinType, val value: Value? = null, val error: EvaluateException? = null)
class ComputedClassNames(val classNames: List<String>, val shouldBeCached: Boolean) {
@Suppress("FunctionName")
companion object {
val EMPTY = ComputedClassNames.Cached(emptyList())
fun Cached(classNames: List<String>) = ComputedClassNames(classNames, true)
fun Cached(className: String) = ComputedClassNames(Collections.singletonList(className), true)
fun NonCached(classNames: List<String>) = ComputedClassNames(classNames, false)
}
fun distinct() = ComputedClassNames(classNames.distinct(), shouldBeCached)
operator fun plus(other: ComputedClassNames) = ComputedClassNames(
classNames + other.classNames, shouldBeCached && other.shouldBeCached
)
}
}
@@ -0,0 +1,26 @@
/*
* 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
const val GENERATED_FUNCTION_NAME = "generated_for_debugger_fun"
const val GENERATED_CLASS_NAME = "Generated_for_debugger_class"
@Suppress("ArrayInDataClass")
data class ClassToLoad(val className: String, val relativeFileName: String, val bytes: ByteArray) {
val isMainClass: Boolean
get() = className == GENERATED_CLASS_NAME
}
@@ -0,0 +1,33 @@
/*
* 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.compilation
import org.jetbrains.kotlin.codegen.CodeFragmentCodegenInfo
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.types.KotlinType
interface CodeFragmentParameter {
val kind: Kind
val name: String
val debugString: String
enum class Kind {
ORDINARY, DELEGATED, EXTENSION_RECEIVER, DISPATCH_RECEIVER, COROUTINE_CONTEXT, LOCAL_FUNCTION,
FAKE_JAVA_OUTER_CLASS, FIELD_VAR, DEBUG_LABEL
}
class Smart(
val dumb: Dumb,
override val targetType: KotlinType,
override val targetDescriptor: DeclarationDescriptor
) : CodeFragmentParameter by dumb, CodeFragmentCodegenInfo.IParameter
data class Dumb(
override val kind: Kind,
override val name: String,
override val debugString: String = name
) : CodeFragmentParameter
}
@@ -0,0 +1,23 @@
/*
* 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.compilation
import com.intellij.debugger.SourcePosition
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad
import org.jetbrains.org.objectweb.asm.Type
data class CompiledDataDescriptor(
val classes: List<ClassToLoad>,
val parameters: List<CodeFragmentParameter.Dumb>,
val crossingBounds: Set<CodeFragmentParameter.Dumb>,
val mainMethodSignature: MethodSignature,
val sourcePosition: SourcePosition?
) {
data class MethodSignature(val parameterTypes: List<Type>, val returnType: Type)
}
val CompiledDataDescriptor.mainClass: ClassToLoad
get() = classes.first { it.isMainClass }
@@ -0,0 +1,102 @@
/*
* 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.evaluation.AbsentInformationEvaluateException
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.jdi.StackFrameProxy
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.sun.jdi.*
fun StackFrameProxyImpl.safeVisibleVariables(): List<LocalVariableProxyImpl> {
return wrapAbsentInformationException { visibleVariables() } ?: emptyList()
}
fun StackFrameProxyImpl.safeVisibleVariableByName(name: String): LocalVariableProxyImpl? {
return wrapAbsentInformationException { visibleVariableByName(name) }
}
fun Method.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun ReferenceType.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun ReferenceType.safeSourceName(): String? {
return wrapAbsentInformationException { sourceName() }
}
fun Method.safeLocationsOfLine(line: Int): List<Location> {
return wrapAbsentInformationException { locationsOfLine(line) } ?: emptyList()
}
fun Method.safeVariables(): List<LocalVariable>? {
return wrapAbsentInformationException { variables() }
}
fun Method.safeArguments(): List<LocalVariable>? {
return wrapAbsentInformationException { arguments() }
}
fun StackFrameProxy.safeLocation(): Location? {
return try {
this.location()
} catch (e: EvaluateException) {
null
}
}
fun Location.safeSourceName(): String? {
return try {
sourceName()
} catch (e: AbsentInformationException) {
null
} catch (e: InternalError) {
null
}
}
fun Location.safeLineNumber(): Int {
return DebuggerUtilsEx.getLineNumber(this, false)
}
fun Location.safeSourceLineNumber(): Int {
return DebuggerUtilsEx.getLineNumber(this, true)
}
fun Location.safeMethod(): Method? {
return DebuggerUtilsEx.getMethod(this)
}
fun LocalVariableProxyImpl.safeType(): Type? {
return wrapClassNotLoadedException { type }
}
fun Field.safeType(): Type? {
return wrapClassNotLoadedException { type() }
}
private inline fun <T> wrapAbsentInformationException(block: () -> T): T? {
return try {
block()
} catch (e: AbsentInformationException) {
null
} catch (e: AbsentInformationEvaluateException) {
null
}
}
private inline fun <T> wrapClassNotLoadedException(block: () -> T): T? {
return try {
block()
} catch (e: ClassNotLoadedException) {
null
}
}
@@ -0,0 +1,96 @@
/*
* 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.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.codegen.inline.FileMapping
import org.jetbrains.kotlin.codegen.inline.SMAP
import org.jetbrains.kotlin.codegen.inline.SMAPParser
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
enum class SourceLineKind {
CALL_LINE,
EXECUTED_LINE
}
fun mapStacktraceLineToSource(smapData: SmapData,
line: Int,
project: Project,
lineKind: SourceLineKind,
searchScope: GlobalSearchScope): Pair<KtFile, Int>? {
val smap = when (lineKind) {
SourceLineKind.CALL_LINE -> smapData.kotlinDebugStrata
SourceLineKind.EXECUTED_LINE -> smapData.kotlinStrata
} ?: return null
val mappingInfo = smap.fileMappings.firstOrNull {
it.getIntervalIfContains(line) != null
} ?: return null
val jvmName = JvmClassName.byInternalName(mappingInfo.path)
val sourceFile = DebuggerUtils.findSourceFileForClassIncludeLibrarySources(
project, searchScope, jvmName, mappingInfo.name) ?: return null
val interval = mappingInfo.getIntervalIfContains(line)!!
val sourceLine = when (lineKind) {
SourceLineKind.CALL_LINE -> interval.source - 1
SourceLineKind.EXECUTED_LINE -> interval.mapDestToSource(line) - 1
}
return sourceFile to sourceLine
}
fun readDebugInfo(bytes: ByteArray): SmapData? {
val cr = ClassReader(bytes)
var debugInfo: String? = null
cr.accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visitSource(source: String?, debug: String?) {
debugInfo = debug
}
}, ClassReader.SKIP_FRAMES and ClassReader.SKIP_CODE)
return debugInfo?.let(::SmapData)
}
class SmapData(debugInfo: String) {
var kotlinStrata: SMAP?
var kotlinDebugStrata: SMAP?
init {
val intervals = debugInfo.split(SMAP.END).filter(String::isNotBlank)
when (intervals.count()) {
1 -> {
kotlinStrata = SMAPParser.parse(intervals[0] + SMAP.END)
kotlinDebugStrata = null
}
2 -> {
kotlinStrata = SMAPParser.parse(intervals[0] + SMAP.END)
kotlinDebugStrata = SMAPParser.parse(intervals[1] + SMAP.END)
}
else -> {
kotlinStrata = null
kotlinDebugStrata = null
}
}
}
}
private fun FileMapping.getIntervalIfContains(destLine: Int) = lineMappings.firstOrNull { it.contains(destLine) }