Move out JVM debugger functionality
This commit is contained in:
@@ -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() }
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
+16
@@ -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"
|
||||
)
|
||||
+255
@@ -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 }
|
||||
}
|
||||
}
|
||||
+359
@@ -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
|
||||
}
|
||||
+52
@@ -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)*$")
|
||||
}
|
||||
+123
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+338
@@ -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"
|
||||
}
|
||||
+74
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -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)
|
||||
}
|
||||
}
|
||||
+238
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
+33
@@ -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
|
||||
}
|
||||
+23
@@ -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 }
|
||||
+102
@@ -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
|
||||
}
|
||||
}
|
||||
+96
@@ -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) }
|
||||
Reference in New Issue
Block a user