Analyze Data Flow: Support dereference processing

#KT-11994 In Progress
This commit is contained in:
Alexey Sedunov
2017-05-31 21:16:45 +03:00
parent 858b454138
commit 0f44dd6ab0
14 changed files with 260 additions and 15 deletions
@@ -28,6 +28,10 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
class KotlinReadWriteAccessDetector : ReadWriteAccessDetector() {
companion object {
val INSTANCE = KotlinReadWriteAccessDetector()
}
override fun isReadWriteAccessible(element: PsiElement) = element is KtVariableDeclaration || element is KtParameter
override fun isDeclarationWriteAccess(element: PsiElement) = isReadWriteAccessible(element)
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.slicer
import com.intellij.psi.PsiElement
import com.intellij.slicer.SliceUsage
import com.intellij.usages.UsagePresentation
import com.intellij.util.Processor
class KotlinSliceDereferenceUsage(
element: PsiElement,
parent: KotlinSliceUsage
) : KotlinSliceUsage(element, parent) {
override fun processChildren(processor: Processor<SliceUsage>) {
// no children
}
override fun getPresentation() = object : UsagePresentation by super.getPresentation() {
override fun getTooltipText() = "Variable dereferenced"
}
}
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.slicer
import com.intellij.slicer.SliceUsage
import com.intellij.slicer.SliceUsageCellRendererBase
import com.intellij.ui.JBColor
import com.intellij.ui.SimpleTextAttributes
import com.intellij.util.FontUtil
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
@@ -38,9 +39,15 @@ object KotlinSliceUsageCellRenderer : SliceUsageCellRendererBase() {
override fun customizeCellRendererFor(sliceUsage: SliceUsage) {
if (sliceUsage !is KotlinSliceUsage) return
val isDereference = sliceUsage is KotlinSliceDereferenceUsage
for ((i, textChunk) in sliceUsage.getText().withIndex()) {
append(textChunk.text, textChunk.simpleAttributesIgnoreBackground)
var attributes = textChunk.simpleAttributesIgnoreBackground
if (isDereference) {
attributes = attributes.derive(attributes.style, JBColor.LIGHT_GRAY, attributes.bgColor, attributes.waveColor)
}
append(textChunk.text, attributes)
if (i == 0) {
append(FontUtil.spaceAndThinSpace())
}
@@ -16,11 +16,14 @@
package org.jetbrains.kotlin.idea.slicer
import com.intellij.codeInsight.highlighting.ReadWriteAccessDetector
import com.intellij.codeInsight.highlighting.ReadWriteAccessDetector.Access
import com.intellij.psi.PsiElement
import com.intellij.psi.search.SearchScope
import com.intellij.slicer.SliceUsage
import com.intellij.usageView.UsageInfo
import com.intellij.util.Processor
import org.jetbrains.kotlin.cfg.pseudocode.PseudoValue
import org.jetbrains.kotlin.cfg.pseudocode.Pseudocode
import org.jetbrains.kotlin.cfg.pseudocode.containingDeclarationForPseudocode
import org.jetbrains.kotlin.cfg.pseudocode.getContainingPseudocode
@@ -37,6 +40,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.findUsages.KotlinFunctionFindUsagesOptions
import org.jetbrains.kotlin.idea.findUsages.KotlinPropertyFindUsagesOptions
import org.jetbrains.kotlin.idea.findUsages.processAllUsages
import org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinReadWriteAccessDetector
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
@@ -58,13 +62,17 @@ private fun KtFunction.processCalls(scope: SearchScope, processor: (UsageInfo) -
)
}
private fun KtDeclaration.processVariableAccesses(scope: SearchScope, isRead: Boolean, processor: (UsageInfo) -> Unit) {
private fun KtDeclaration.processVariableAccesses(
scope: SearchScope,
kind: Access,
processor: (UsageInfo) -> Unit
) {
processAllUsages(
{
KotlinPropertyFindUsagesOptions(project).apply {
isReadAccess = isRead
isWriteAccess = !isRead
isReadWriteAccess = false
isReadAccess = kind == Access.Read || kind == Access.ReadWrite
isWriteAccess = kind == Access.Write || kind == Access.ReadWrite
isReadWriteAccess = kind == Access.ReadWrite
isSearchForTextOccurrences = false
isSkipImportStatements = true
searchScope = scope.intersectWith(useScope)
@@ -88,7 +96,7 @@ private fun KtParameter.canProcess(): Boolean {
abstract class Slicer(
protected val element: KtExpression,
protected val processor: Processor<SliceUsage>,
protected val parentUsage: SliceUsage
protected val parentUsage: KotlinSliceUsage
) {
protected class PseudocodeCache {
private val computedPseudocodes = HashMap<KtElement, Pseudocode>()
@@ -113,13 +121,13 @@ abstract class Slicer(
class InflowSlicer(
element: KtExpression,
processor: Processor<SliceUsage>,
parentUsage: SliceUsage
parentUsage: KotlinSliceUsage
) : Slicer(element, processor, parentUsage) {
private fun KtProperty.processProperty() {
if (!canProcess()) return
initializer?.passToProcessor()
processVariableAccesses(parentUsage.scope.toSearchScope(), false) body@ {
processVariableAccesses(parentUsage.scope.toSearchScope(), Access.Write) body@ {
val refExpression = it.element as? KtExpression ?: return@body
val rhs = KtPsiUtil.safeDeparenthesize(refExpression).getAssignmentByLHS()?.right ?: return@body
rhs.passToProcessor()
@@ -192,7 +200,7 @@ class InflowSlicer(
class OutflowSlicer(
element: KtExpression,
processor: Processor<SliceUsage>,
parentUsage: SliceUsage
parentUsage: KotlinSliceUsage
) : Slicer(element, processor, parentUsage) {
private fun KtDeclaration.processVariable() {
when (this) {
@@ -200,9 +208,18 @@ class OutflowSlicer(
is KtParameter -> if (!canProcess()) return
else -> return
}
processVariableAccesses(parentUsage.scope.toSearchScope(),true) body@ {
val withDereferences = parentUsage.params.showInstanceDereferences
processVariableAccesses(
parentUsage.scope.toSearchScope(),
if (withDereferences) Access.ReadWrite else Access.Read
) body@ {
val refExpression = (it.element as? KtExpression)?.let { KtPsiUtil.safeDeparenthesize(it) } ?: return@body
refExpression.processExpression()
if (withDereferences) {
refExpression.processDereferences()
}
if (!withDereferences || KotlinReadWriteAccessDetector.INSTANCE.getExpressionAccess(refExpression) == Access.Read) {
refExpression.processExpression()
}
}
}
@@ -224,17 +241,49 @@ class OutflowSlicer(
}
}
private fun KtExpression.processExpression() {
private fun processDereferenceIsNeeded(
expression: KtExpression,
pseudoValue: PseudoValue,
instr: InstructionWithReceivers
) {
if (!parentUsage.params.showInstanceDereferences) return
val receiver = instr.receiverValues[pseudoValue]
val resolvedCall = when (instr) {
is CallInstruction -> instr.resolvedCall
is ReadValueInstruction -> (instr.target as? AccessTarget.Call)?.resolvedCall
else -> null
} ?: return
if (receiver != null && resolvedCall.dispatchReceiver == receiver) {
processor.process(KotlinSliceDereferenceUsage(expression, parentUsage))
}
}
private fun KtExpression.processPseudocodeUsages(processor: (PseudoValue, Instruction) -> Unit) {
val pseudocode = pseudocodeCache[this] ?: return
val pseudoValue = pseudocode.getElementValue(this) ?: return
pseudocode.getUsages(pseudoValue).forEach { instr ->
pseudocode.getUsages(pseudoValue).forEach { processor(pseudoValue, it) }
}
private fun KtExpression.processDereferences() {
processPseudocodeUsages { pseudoValue, instr ->
when (instr) {
is ReadValueInstruction -> processDereferenceIsNeeded(this, pseudoValue, instr)
is CallInstruction -> processDereferenceIsNeeded(this, pseudoValue, instr)
}
}
}
private fun KtExpression.processExpression() {
processPseudocodeUsages { pseudoValue, instr ->
when (instr) {
is WriteValueInstruction -> (instr.target.accessedDescriptor?.source?.getPsi() as? KtDeclaration)?.passToProcessor()
is CallInstruction -> instr.arguments[pseudoValue]?.source?.getPsi()?.passToProcessor()
is ReturnValueInstruction -> (instr.owner.correspondingElement as? KtFunction)?.passToProcessor()
is MagicInstruction -> when (instr.kind) {
MagicKind.NOT_NULL_ASSERTION, MagicKind.CAST -> instr.outputValue.element?.passToProcessor()
else -> return
else -> {}
}
}
}
@@ -0,0 +1,21 @@
// FLOW: OUT
// WITH_DEREFERENCES
class A
operator fun A.get(i: Int) = this
operator fun A.set(i: Int, a: A) = this
operator fun A.plusAssign(a: A) = this
operator fun A.times(a: A) = this
operator fun A.inc() = this
fun test() {
val <caret>x = A()
val y = A()
x[1]
x[1] = y
x[1] += y
x[1] *= y
x[1]++
}
@@ -0,0 +1 @@
13 val <bold>x = A()</bold>
+21
View File
@@ -0,0 +1,21 @@
// FLOW: OUT
// WITH_DEREFERENCES
class A {
operator fun get(i: Int) = this
operator fun set(i: Int, a: A) = this
operator fun plusAssign(a: A) = this
operator fun times(a: A) = this
operator fun inc() = this
}
fun test() {
val <caret>x = A()
val y = A()
x[1]
x[1] = y
x[1] += y
x[1] *= y
x[1]++
}
@@ -0,0 +1,6 @@
13 val <bold>x = A()</bold>
16 DEREFERENCE: <bold>x</bold>[1]
17 DEREFERENCE: <bold>x</bold>[1] = y
18 DEREFERENCE: <bold>x</bold>[1] += y
19 DEREFERENCE: <bold>x</bold>[1] *= y
20 DEREFERENCE: <bold>x</bold>[1]++
@@ -0,0 +1,30 @@
// FLOW: OUT
// WITH_DEREFERENCES
class A {
operator fun plus(n: Int) = this
operator fun unaryPlus() = this
operator fun inc() = this
operator fun timesAssign(n: Int) = this
}
operator fun A.minus(n: Int) = this
operator fun A.unaryMinus() = this
operator fun A.dec() = this
operator fun A.divAssign(n: Int) = this
fun test() {
var <caret>x = A()
+x
x + 1
x++
x += 1
x *= 1
-x
x - 1
x--
x -= 1
x /= 1
}
@@ -0,0 +1,6 @@
17 var <bold>x = A()</bold>
19 DEREFERENCE: +<bold>x</bold>
20 DEREFERENCE: <bold>x</bold> + 1
21 DEREFERENCE: <bold>x</bold>++
22 DEREFERENCE: <bold>x</bold> += 1
23 DEREFERENCE: <bold>x</bold> *= 1
+26
View File
@@ -0,0 +1,26 @@
// FLOW: OUT
// WITH_DEREFERENCES
class A {
fun foo() = 1
val bar = 2
}
fun A.fooExt() = 1
val A.barExt: Int get() = 2
fun test() {
val <caret>x = A()
x.foo()
x.bar
x.fooExt()
x.barExt
val y: A? = x
y?.foo()
y?.bar
y?.fooExt()
y?.barExt
}
@@ -0,0 +1,6 @@
13 val <bold>x = A()</bold>
15 DEREFERENCE: <bold>x</bold>.foo()
16 DEREFERENCE: <bold>x</bold>.bar
20 val <bold>y: A? = x</bold>
22 DEREFERENCE: <bold>y</bold>?.foo()
23 DEREFERENCE: <bold>y</bold>?.bar
@@ -40,6 +40,8 @@ abstract class AbstractSlicerTest : KotlinLightCodeInsightFixtureTestCase() {
// Based on SliceUsage.processChildren
private fun KotlinSliceUsage.processChildrenWithoutProgress(processor: (KotlinSliceUsage) -> Unit) {
if (this is KotlinSliceDereferenceUsage) return
val element = runReadAction { element }
val uniqueProcessor = CommonProcessors.UniqueProcessor(
@@ -78,6 +80,9 @@ abstract class AbstractSlicerTest : KotlinLightCodeInsightFixtureTestCase() {
val chunks = usage.text
append(chunks.first().render() + " ")
append("\t".repeat(indent))
if (usage is KotlinSliceDereferenceUsage) {
append("DEREFERENCE: ")
}
chunks.slice(1..chunks.size - 1).joinTo(
this,
separator = "",
@@ -85,7 +90,9 @@ abstract class AbstractSlicerTest : KotlinLightCodeInsightFixtureTestCase() {
postfix = "\n"
) { it.render() }
if (!isDuplicated) {
usage.processChildrenWithoutProgress { append(process(it, indent + 1)) }
usage.processChildrenWithoutProgress {
append(process(it, indent + 1))
}
}
}.replace(Regex("</bold><bold>"), "")
}
@@ -100,12 +107,14 @@ abstract class AbstractSlicerTest : KotlinLightCodeInsightFixtureTestCase() {
val fileText = FileUtil.loadFile(mainFile, true)
val flowKind = InTextDirectivesUtils.findStringWithPrefixes(fileText, "// FLOW: ")
val withDereferences = InTextDirectivesUtils.isDirectiveDefined(fileText, "// WITH_DEREFERENCES")
val analysisParams = SliceAnalysisParams().apply {
dataFlowToThis = when (flowKind) {
"IN" -> true
"OUT" -> false
else -> throw AssertionError("Invalid flow kind: $flowKind")
}
showInstanceDereferences = withDereferences
scope = AnalysisScope(project)
}
@@ -174,6 +174,12 @@ public class SlicerTestGenerated extends AbstractSlicerTest {
doTest(fileName);
}
@TestMetadata("outflow/extensionIndexingDereferences.kt")
public void testOutflow_ExtensionIndexingDereferences() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/extensionIndexingDereferences.kt");
doTest(fileName);
}
@TestMetadata("outflow/funBodyExpression.kt")
public void testOutflow_FunBodyExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/funBodyExpression.kt");
@@ -210,6 +216,12 @@ public class SlicerTestGenerated extends AbstractSlicerTest {
doTest(fileName);
}
@TestMetadata("outflow/indexingDereferences.kt")
public void testOutflow_IndexingDereferences() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/indexingDereferences.kt");
doTest(fileName);
}
@TestMetadata("outflow/localVariableUsages.kt")
public void testOutflow_LocalVariableUsages() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/localVariableUsages.kt");
@@ -234,6 +246,12 @@ public class SlicerTestGenerated extends AbstractSlicerTest {
doTest(fileName);
}
@TestMetadata("outflow/operatorCallDereferences.kt")
public void testOutflow_OperatorCallDereferences() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/operatorCallDereferences.kt");
doTest(fileName);
}
@TestMetadata("outflow/primaryConstructorCalls.kt")
public void testOutflow_PrimaryConstructorCalls() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/primaryConstructorCalls.kt");
@@ -252,6 +270,12 @@ public class SlicerTestGenerated extends AbstractSlicerTest {
doTest(fileName);
}
@TestMetadata("outflow/simpleCallDereferences.kt")
public void testOutflow_SimpleCallDereferences() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/simpleCallDereferences.kt");
doTest(fileName);
}
@TestMetadata("outflow/topLevelPropertyUsages.kt")
public void testOutflow_TopLevelPropertyUsages() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/slicer/outflow/topLevelPropertyUsages.kt");