Analyze Data Flow: Support dereference processing
#KT-11994 In Progress
This commit is contained in:
+4
@@ -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>
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user