Skip to content

Commit 623ee31

Browse files
committed
Fix step over for inlined functions in Android Studio (KT-14374)
#KT-14374 Fixed
1 parent f87779b commit 623ee31

28 files changed

+392
-79
lines changed

idea/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,37 +101,40 @@ class KotlinPositionManager(private val myDebugProcess: DebugProcess) : MultiReq
101101
throw NoDataException.INSTANCE
102102
}
103103

104-
val lineNumber = try {
104+
val sourceLineNumber = try {
105105
location.lineNumber() - 1
106106
}
107107
catch (e: InternalError) {
108108
-1
109109
}
110110

111-
if (lineNumber < 0) {
111+
if (sourceLineNumber < 0) {
112112
throw NoDataException.INSTANCE
113113
}
114114

115-
val lambdaOrFunIfInside = getLambdaOrFunIfInside(location, psiFile as KtFile, lineNumber)
115+
val lambdaOrFunIfInside = getLambdaOrFunIfInside(location, psiFile as KtFile, sourceLineNumber)
116116
if (lambdaOrFunIfInside != null) {
117117
return SourcePosition.createFromElement(lambdaOrFunIfInside.bodyExpression!!)
118118
}
119-
val elementInDeclaration = getElementForDeclarationLine(location, psiFile, lineNumber)
119+
val elementInDeclaration = getElementForDeclarationLine(location, psiFile, sourceLineNumber)
120120
if (elementInDeclaration != null) {
121121
return SourcePosition.createFromElement(elementInDeclaration)
122122
}
123123

124-
if (lineNumber > psiFile.getLineCount() && myDebugProcess.isDexDebug()) {
125-
val inlinePosition = getOriginalPositionOfInlinedLine(
126-
location.lineNumber(), FqName(location.declaringType().name()), location.sourceName(),
127-
myDebugProcess.project, GlobalSearchScope.allScope(myDebugProcess.project))
124+
if (sourceLineNumber > psiFile.getLineCount() && myDebugProcess.isDexDebug()) {
125+
val thisFunLine = getLastLineNumberForLocation(location, myDebugProcess.project)
126+
if (thisFunLine != null && thisFunLine != location.lineNumber()) {
127+
return SourcePosition.createFromLine(psiFile, thisFunLine - 1)
128+
}
129+
130+
val inlinePosition = getOriginalPositionOfInlinedLine(location, myDebugProcess.project)
128131

129132
if (inlinePosition != null) {
130133
return SourcePosition.createFromLine(inlinePosition.first, inlinePosition.second)
131134
}
132135
}
133136

134-
return SourcePosition.createFromLine(psiFile, lineNumber)
137+
return SourcePosition.createFromLine(psiFile, sourceLineNumber)
135138
}
136139

137140
// Returns a property or a constructor if debugger stops at class declaration

idea/src/org/jetbrains/kotlin/idea/debugger/NoStrataPositionManagerHelper.kt

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import com.intellij.debugger.engine.DebugProcess
2121
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
2222
import com.intellij.openapi.application.ApplicationManager
2323
import com.intellij.openapi.project.Project
24+
import com.intellij.openapi.vfs.VirtualFile
2425
import com.intellij.psi.search.GlobalSearchScope
2526
import com.sun.jdi.Location
2627
import com.sun.jdi.ReferenceType
28+
import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil
2729
import org.jetbrains.kotlin.idea.refactoring.getLineStartOffset
2830
import org.jetbrains.kotlin.idea.util.application.runReadAction
2931
import org.jetbrains.kotlin.lexer.KtTokens
@@ -32,19 +34,92 @@ import org.jetbrains.kotlin.psi.KtFile
3234
import org.jetbrains.kotlin.psi.KtFunction
3335
import org.jetbrains.kotlin.psi.psiUtil.parents
3436
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
37+
import org.jetbrains.kotlin.utils.getOrPutNullable
38+
import org.jetbrains.org.objectweb.asm.*
39+
import java.util.*
40+
41+
// TODO: Don't read same bytecode file again and again
42+
// TODO: Build line mapping for the whole file
43+
// TODO: Quick caching for the same location
44+
45+
fun noStrataLineNumber(location: Location, isDexDebug: Boolean, project: Project, preferInlined: Boolean = false): Int {
46+
if (isDexDebug) {
47+
if (!preferInlined) {
48+
val thisFunLine = runReadAction { getLastLineNumberForLocation(location, project) }
49+
if (thisFunLine != null && thisFunLine != location.lineNumber()) {
50+
// TODO: bad line because of inlining
51+
return thisFunLine
52+
}
53+
}
54+
55+
val inlinePosition = runReadAction { getOriginalPositionOfInlinedLine(location, project) }
56+
57+
if (inlinePosition != null) {
58+
return inlinePosition.second + 1
59+
}
60+
}
61+
62+
return location.lineNumber()
63+
}
64+
65+
fun getLastLineNumberForLocation(location: Location, project: Project, searchScope: GlobalSearchScope = GlobalSearchScope.allScope(project)): Int? {
66+
val lineNumber = location.lineNumber()
67+
val fqName = FqName(location.declaringType().name())
68+
val fileName = location.sourceName()
69+
70+
val method = location.method() ?: return null
71+
72+
val bytes = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null
73+
74+
fun readLineNumberTableMapping(bytes: ByteArray): Map<String, Set<Int>> {
75+
val labelsToAllStrings = HashMap<String, MutableSet<Int>>()
3576

36-
internal fun getOriginalPositionOfInlinedLine(
37-
lineNumber: Int, fqName: FqName, fileName: String, project: Project, searchScope: GlobalSearchScope): Pair<KtFile, Int>? {
77+
ClassReader(bytes).accept(object : ClassVisitor(InlineCodegenUtil.API) {
78+
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
79+
if (!(name == method.name() && desc == method.signature())) {
80+
return null
81+
}
82+
83+
return object : MethodVisitor(Opcodes.ASM5, null) {
84+
override fun visitLineNumber(line: Int, start: Label?) {
85+
if (start != null) {
86+
labelsToAllStrings.getOrPutNullable(start.toString(), { LinkedHashSet<Int>() }).add(line)
87+
}
88+
}
89+
}
90+
}
91+
}, ClassReader.SKIP_FRAMES and ClassReader.SKIP_CODE)
92+
93+
return labelsToAllStrings
94+
}
95+
96+
val lineMapping = readLineNumberTableMapping(bytes)
97+
return lineMapping.values.firstOrNull { it.contains(lineNumber) }?.last()
98+
}
99+
100+
internal fun getOriginalPositionOfInlinedLine(location: Location, project: Project): Pair<KtFile, Int>? {
101+
val lineNumber = location.lineNumber()
102+
val fqName = FqName(location.declaringType().name())
103+
val fileName = location.sourceName()
104+
val searchScope = GlobalSearchScope.allScope(project)
105+
106+
val bytes = findAndReadClassFile(fqName, fileName, project, searchScope, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null
107+
val smapData = readDebugInfo(bytes) ?: return null
108+
return mapStacktraceLineToSource(smapData, lineNumber, project, SourceLineKind.EXECUTED_LINE, searchScope)
109+
}
110+
111+
internal fun findAndReadClassFile(
112+
fqName: FqName, fileName: String, project: Project, searchScope: GlobalSearchScope,
113+
sourceFileFilter: (VirtualFile) -> Boolean = { true },
114+
libFileFilter: (VirtualFile) -> Boolean = { true }): ByteArray? {
38115
val internalName = fqName.asString().replace('.', '/')
39116
val jvmClassName = JvmClassName.byInternalName(internalName)
40117

41118
val file = DebuggerUtils.findSourceFileForClassIncludeLibrarySources(project, searchScope, jvmClassName, fileName) ?: return null
42119

43120
val virtualFile = file.virtualFile ?: return null
44121

45-
val bytes = readClassFile(project, jvmClassName, virtualFile, { isInlineFunctionLineNumber(it, lineNumber, project) }) ?: return null
46-
val smapData = readDebugInfo(bytes) ?: return null
47-
return mapStacktraceLineToSource(smapData, lineNumber, project, SourceLineKind.EXECUTED_LINE, searchScope)
122+
return readClassFile(project, jvmClassName, virtualFile, sourceFileFilter = sourceFileFilter, libFileFilter = libFileFilter)
48123
}
49124

50125
internal fun getLocationsOfInlinedLine(type: ReferenceType, position: SourcePosition, sourceSearchScope: GlobalSearchScope): List<Location> {

idea/src/org/jetbrains/kotlin/idea/debugger/stepping/DebuggerSteppingHelper.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,16 @@ public static DebugProcessImpl.ResumeCommand createStepOverCommand(
5555
return debugProcess.new ResumeCommand(suspendContext) {
5656
@Override
5757
public void contextAction() {
58-
// In DEX there's no strata for trying to understand what lines were inlined and no local variables attributes are
59-
// preserved that can be used for fine tuning. So do an ordinal 'step over' instead.
60-
if (NoStrataPositionManagerHelperKt.isDexDebug(suspendContext.getDebugProcess())) {
61-
debugProcess.createStepOverCommand(suspendContext, true).contextAction();
62-
return;
63-
}
58+
boolean isDexDebug = NoStrataPositionManagerHelperKt.isDexDebug(suspendContext.getDebugProcess());
6459

6560
try {
6661
StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
6762
if (frameProxy != null) {
6863
Action action = KotlinSteppingCommandProviderKt.getStepOverAction(
6964
frameProxy.location(),
7065
kotlinSourcePosition,
71-
frameProxy
66+
frameProxy,
67+
isDexDebug
7268
);
7369

7470
createStepRequest(

idea/src/org/jetbrains/kotlin/idea/debugger/stepping/KotlinStepOverInlineFilter.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,24 @@ package org.jetbrains.kotlin.idea.debugger.stepping
1818

1919
import com.intellij.debugger.engine.DebugProcessImpl
2020
import com.intellij.debugger.engine.SuspendContextImpl
21+
import com.intellij.openapi.project.Project
2122
import com.intellij.util.Range
2223
import com.sun.jdi.LocalVariable
2324
import com.sun.jdi.Location
25+
import org.jetbrains.kotlin.idea.debugger.noStrataLineNumber
2426

25-
class KotlinStepOverInlineFilter(val stepOverLines: Set<Int>, val fromLine: Int,
26-
val inlineFunRangeVariables: List<LocalVariable>) : KotlinMethodFilter {
27-
override fun locationMatches(context: SuspendContextImpl, location: Location): Boolean {
28-
val frameProxy = context.frameProxy
29-
if (frameProxy == null) return true
27+
class KotlinStepOverInlineFilter(
28+
val isDexDebug: Boolean,
29+
val project: Project,
30+
val stepOverLines: Set<Int>, val fromLine: Int,
31+
val inlineFunRangeVariables: List<LocalVariable>) : KotlinMethodFilter {
32+
private fun Location.ktLineNumber() = noStrataLineNumber(this, isDexDebug, project)
3033

31-
val currentLine = location.lineNumber()
34+
override fun locationMatches(context: SuspendContextImpl, location: Location): Boolean {
35+
val frameProxy = context.frameProxy ?: return true
3236

33-
if (!(stepOverLines.contains(location.lineNumber()))) {
37+
val currentLine = location.ktLineNumber()
38+
if (!(stepOverLines.contains(currentLine))) {
3439
return currentLine != fromLine
3540
}
3641

idea/src/org/jetbrains/kotlin/idea/debugger/stepping/KotlinSteppingCommandProvider.kt

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
4040
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
4141
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
4242
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils
43+
import org.jetbrains.kotlin.idea.debugger.noStrataLineNumber
4344
import org.jetbrains.kotlin.idea.refactoring.getLineEndOffset
4445
import org.jetbrains.kotlin.idea.refactoring.getLineNumber
4546
import org.jetbrains.kotlin.idea.refactoring.getLineStartOffset
@@ -268,12 +269,13 @@ private fun findCallsOnPosition(sourcePosition: SourcePosition, filter: (KtCallE
268269
sealed class Action(val position: XSourcePositionImpl? = null,
269270
val lineNumber: Int? = null,
270271
val stepOverLines: Set<Int>? = null,
271-
val inlineRangeVariables: List<LocalVariable>? = null) {
272+
val inlineRangeVariables: List<LocalVariable>? = null,
273+
val isDexDebug: Boolean = false) {
272274
class STEP_OVER : Action()
273275
class STEP_OUT : Action()
274276
class RUN_TO_CURSOR(position: XSourcePositionImpl) : Action(position)
275-
class STEP_OVER_INLINED(lineNumber: Int, stepOverLines: Set<Int>, inlineVariables: List<LocalVariable>) : Action(
276-
lineNumber = lineNumber, stepOverLines = stepOverLines, inlineRangeVariables = inlineVariables)
277+
class STEP_OVER_INLINED(lineNumber: Int, stepOverLines: Set<Int>, inlineVariables: List<LocalVariable>, isDexDebug: Boolean) : Action(
278+
lineNumber = lineNumber, stepOverLines = stepOverLines, inlineRangeVariables = inlineVariables, isDexDebug = isDexDebug)
277279

278280
fun apply(debugProcess: DebugProcessImpl,
279281
suspendContext: SuspendContextImpl,
@@ -287,39 +289,54 @@ sealed class Action(val position: XSourcePositionImpl? = null,
287289
is Action.STEP_OUT -> debugProcess.createStepOutCommand(suspendContext).contextAction(suspendContext)
288290
is Action.STEP_OVER -> debugProcess.createStepOverCommand(suspendContext, ignoreBreakpoints).contextAction(suspendContext)
289291
is Action.STEP_OVER_INLINED -> KotlinStepActionFactory(debugProcess).createKotlinStepOverInlineAction(
290-
KotlinStepOverInlineFilter(stepOverLines!!, lineNumber ?: -1, inlineRangeVariables!!)).contextAction(suspendContext)
292+
KotlinStepOverInlineFilter(
293+
isDexDebug,
294+
debugProcess.project,
295+
stepOverLines!!,
296+
lineNumber ?: -1,
297+
inlineRangeVariables!!)).contextAction(suspendContext)
291298
}
292299
}
293300
}
294301

295-
interface KotlinMethodFilter: MethodFilter {
302+
interface KotlinMethodFilter : MethodFilter {
296303
fun locationMatches(context: SuspendContextImpl, location: Location): Boolean
297304
}
298305

299306
fun getStepOverAction(
300307
location: Location,
301308
kotlinSourcePosition: KotlinSteppingCommandProvider.KotlinSourcePosition,
302-
frameProxy: StackFrameProxyImpl
309+
frameProxy: StackFrameProxyImpl,
310+
isDexDebug: Boolean
303311
): Action {
304312
val inlineArgumentsToSkip = runReadAction {
305313
getInlineCallFunctionArgumentsIfAny(kotlinSourcePosition.sourcePosition)
306314
}
307315

308316
return getStepOverAction(location, kotlinSourcePosition.file, kotlinSourcePosition.linesRange,
309-
inlineArgumentsToSkip, frameProxy)
317+
inlineArgumentsToSkip, frameProxy, isDexDebug)
310318
}
311319

312320
fun getStepOverAction(
313321
location: Location,
314322
file: KtFile,
315323
range: IntRange,
316324
inlineFunctionArguments: List<KtElement>,
317-
frameProxy: StackFrameProxyImpl
325+
frameProxy: StackFrameProxyImpl,
326+
isDexDebug: Boolean
318327
): Action {
319328
val computedReferenceType = location.declaringType() ?: return Action.STEP_OVER()
320329

330+
val project = file.project
331+
332+
fun Location.ktLineNumber() = noStrataLineNumber(this, isDexDebug, project, true)
333+
321334
fun isLocationSuitable(nextLocation: Location): Boolean {
322-
if (nextLocation.method() != location.method() || nextLocation.lineNumber() !in range) {
335+
if (nextLocation.method() != location.method()) {
336+
return false
337+
}
338+
339+
if (nextLocation.ktLineNumber() !in range) {
323340
return false
324341
}
325342

@@ -336,22 +353,24 @@ fun getStepOverAction(
336353
.dropWhile { it != location }
337354
.drop(1)
338355
.filter(::isLocationSuitable)
339-
.dropWhile { it.lineNumber() == location.lineNumber() }
356+
.dropWhile { it.ktLineNumber() == location.ktLineNumber() }
340357
.firstOrNull()
341358

342-
return previousSuitableLocation != null && previousSuitableLocation.lineNumber() > location.lineNumber()
359+
return previousSuitableLocation != null && previousSuitableLocation.ktLineNumber() > location.ktLineNumber()
343360
}
344361

345362
val patchedLocation = if (isBackEdgeLocation()) {
346363
// Pretend we had already did a backing step
347364
computedReferenceType.allLineLocations()
348365
.filter(::isLocationSuitable)
349-
.first { it.lineNumber() == location.lineNumber() }
366+
.first { it.ktLineNumber() == location.ktLineNumber() }
350367
}
351368
else {
352369
location
353370
}
354371

372+
val patchedLineNumber = patchedLocation.ktLineNumber()
373+
355374
val lambdaArgumentRanges = runReadAction {
356375
inlineFunctionArguments.filterIsInstance<KtElement>().map {
357376
val startLineNumber = it.getLineNumber(true) + 1
@@ -367,21 +386,24 @@ fun getStepOverAction(
367386
// - Lines from other files and from functions that are not in range of current one are definitely inlined
368387
// - Lines in function arguments of inlined functions are inlined too as we found them starting from the position of inlined call.
369388
//
370-
// This heuristic doesn't work for DEX, because of missing strata information (https://code.google.com/p/android/issues/detail?id=82972)
371-
//
372389
// It also thinks that too many lines are inlined when there's a call of function argument or other
373390
// inline function in last statement of inline function. The list of inlineRangeVariables is used to overcome it.
374391
val probablyInlinedLocations = computedReferenceType.allLineLocations()
375392
.dropWhile { it != patchedLocation }
376393
.drop(1)
377-
.dropWhile { it.lineNumber() == patchedLocation.lineNumber() }
378-
.takeWhile { locationAtLine ->
379-
!isLocationSuitable(locationAtLine) || lambdaArgumentRanges.any { locationAtLine.lineNumber() in it }
394+
.dropWhile { it.ktLineNumber() == patchedLineNumber }
395+
.takeWhile { location ->
396+
!isLocationSuitable(location) || lambdaArgumentRanges.any { location.ktLineNumber() in it }
380397
}
381-
.dropWhile { it.lineNumber() == patchedLocation.lineNumber() }
398+
.dropWhile { it.ktLineNumber() == patchedLineNumber }
382399

383400
if (!probablyInlinedLocations.isEmpty()) {
384-
return Action.STEP_OVER_INLINED(patchedLocation.lineNumber(), probablyInlinedLocations.map { it.lineNumber() }.toSet(), inlineRangeVariables)
401+
return Action.STEP_OVER_INLINED(
402+
patchedLineNumber,
403+
probablyInlinedLocations.map { it.ktLineNumber() }.toSet(),
404+
inlineRangeVariables,
405+
isDexDebug
406+
)
385407
}
386408

387409
return Action.STEP_OVER()

idea/testData/debugger/tinyApp/outs/inlineInIfFalseDex.out

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ LineBreakpoint created at inlineInIfFalseDex.kt:6
22
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! inlineInIfFalseDex.InlineInIfFalseDexKt
33
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
44
inlineInIfFalseDex.kt:6
5-
inlineInIfFalseDex.kt:15
65
inlineInIfFalseDex.kt:9
76
inlineInIfFalseDex.kt:10
87
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

idea/testData/debugger/tinyApp/outs/inlineInIfTrueDex.out

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ LineBreakpoint created at inlineInIfTrueDex.kt:6
22
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! inlineInIfTrueDex.InlineInIfTrueDexKt
33
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
44
inlineInIfTrueDex.kt:6
5-
inlineInIfTrueDex.kt:14
65
inlineInIfTrueDex.kt:7
76
inlineInIfTrueDex.kt:9
87
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'

idea/testData/debugger/tinyApp/outs/soInlineAnonymousFunctionArgumentDex.out

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,8 @@ LineBreakpoint created at soInlineAnonymousFunctionArgumentDex.kt:5
33
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
44
soInlineAnonymousFunctionArgumentDex.kt:5
55
soInlineAnonymousFunctionArgumentDex.kt:7
6-
soInlineAnonymousFunctionArgumentDex.kt:15
7-
soInlineAnonymousFunctionArgumentDex.kt:8
8-
soInlineAnonymousFunctionArgumentDex.kt:16
9-
soInlineAnonymousFunctionArgumentDex.kt:15
106
soInlineAnonymousFunctionArgumentDex.kt:11
11-
soInlineAnonymousFunctionArgumentDex.kt:16
7+
soInlineAnonymousFunctionArgumentDex.kt:12
128
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
139

1410
Process finished with exit code 0

0 commit comments

Comments
 (0)