diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/ideaExtensions/KotlinReadWriteAccessDetector.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/ideaExtensions/KotlinReadWriteAccessDetector.kt index aa10b07ea3d..d8d83adfe70 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/ideaExtensions/KotlinReadWriteAccessDetector.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/ideaExtensions/KotlinReadWriteAccessDetector.kt @@ -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) diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceDereferenceUsage.kt b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceDereferenceUsage.kt new file mode 100644 index 00000000000..d8390b69b1f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceDereferenceUsage.kt @@ -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) { + // no children + } + + override fun getPresentation() = object : UsagePresentation by super.getPresentation() { + override fun getTooltipText() = "Variable dereferenced" + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceUsageCellRenderer.kt b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceUsageCellRenderer.kt index d98716a3856..878ae90b268 100644 --- a/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceUsageCellRenderer.kt +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceUsageCellRenderer.kt @@ -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()) } diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/Slicer.kt b/idea/src/org/jetbrains/kotlin/idea/slicer/Slicer.kt index 5cf64ffde49..ae190319a96 100644 --- a/idea/src/org/jetbrains/kotlin/idea/slicer/Slicer.kt +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/Slicer.kt @@ -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, - protected val parentUsage: SliceUsage + protected val parentUsage: KotlinSliceUsage ) { protected class PseudocodeCache { private val computedPseudocodes = HashMap() @@ -113,13 +121,13 @@ abstract class Slicer( class InflowSlicer( element: KtExpression, processor: Processor, - 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, - 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 -> {} } } } diff --git a/idea/testData/slicer/outflow/extensionIndexingDereferences.kt b/idea/testData/slicer/outflow/extensionIndexingDereferences.kt new file mode 100644 index 00000000000..aa035f5c241 --- /dev/null +++ b/idea/testData/slicer/outflow/extensionIndexingDereferences.kt @@ -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 x = A() + val y = A() + + x[1] + x[1] = y + x[1] += y + x[1] *= y + x[1]++ +} \ No newline at end of file diff --git a/idea/testData/slicer/outflow/extensionIndexingDereferences.results.txt b/idea/testData/slicer/outflow/extensionIndexingDereferences.results.txt new file mode 100644 index 00000000000..80d3aa11b0a --- /dev/null +++ b/idea/testData/slicer/outflow/extensionIndexingDereferences.results.txt @@ -0,0 +1 @@ +13 val x = A() diff --git a/idea/testData/slicer/outflow/indexingDereferences.kt b/idea/testData/slicer/outflow/indexingDereferences.kt new file mode 100644 index 00000000000..075a274c1af --- /dev/null +++ b/idea/testData/slicer/outflow/indexingDereferences.kt @@ -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 x = A() + val y = A() + + x[1] + x[1] = y + x[1] += y + x[1] *= y + x[1]++ +} \ No newline at end of file diff --git a/idea/testData/slicer/outflow/indexingDereferences.results.txt b/idea/testData/slicer/outflow/indexingDereferences.results.txt new file mode 100644 index 00000000000..4b68a49ff75 --- /dev/null +++ b/idea/testData/slicer/outflow/indexingDereferences.results.txt @@ -0,0 +1,6 @@ +13 val x = A() +16 DEREFERENCE: x[1] +17 DEREFERENCE: x[1] = y +18 DEREFERENCE: x[1] += y +19 DEREFERENCE: x[1] *= y +20 DEREFERENCE: x[1]++ diff --git a/idea/testData/slicer/outflow/operatorCallDereferences.kt b/idea/testData/slicer/outflow/operatorCallDereferences.kt new file mode 100644 index 00000000000..6ca9445300e --- /dev/null +++ b/idea/testData/slicer/outflow/operatorCallDereferences.kt @@ -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 x = A() + + +x + x + 1 + x++ + x += 1 + x *= 1 + + -x + x - 1 + x-- + x -= 1 + x /= 1 +} \ No newline at end of file diff --git a/idea/testData/slicer/outflow/operatorCallDereferences.results.txt b/idea/testData/slicer/outflow/operatorCallDereferences.results.txt new file mode 100644 index 00000000000..4311e539cf3 --- /dev/null +++ b/idea/testData/slicer/outflow/operatorCallDereferences.results.txt @@ -0,0 +1,6 @@ +17 var x = A() +19 DEREFERENCE: +x +20 DEREFERENCE: x + 1 +21 DEREFERENCE: x++ +22 DEREFERENCE: x += 1 +23 DEREFERENCE: x *= 1 diff --git a/idea/testData/slicer/outflow/simpleCallDereferences.kt b/idea/testData/slicer/outflow/simpleCallDereferences.kt new file mode 100644 index 00000000000..b201416da31 --- /dev/null +++ b/idea/testData/slicer/outflow/simpleCallDereferences.kt @@ -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 x = A() + + x.foo() + x.bar + x.fooExt() + x.barExt + + val y: A? = x + + y?.foo() + y?.bar + y?.fooExt() + y?.barExt +} \ No newline at end of file diff --git a/idea/testData/slicer/outflow/simpleCallDereferences.results.txt b/idea/testData/slicer/outflow/simpleCallDereferences.results.txt new file mode 100644 index 00000000000..06624e9116b --- /dev/null +++ b/idea/testData/slicer/outflow/simpleCallDereferences.results.txt @@ -0,0 +1,6 @@ +13 val x = A() +15 DEREFERENCE: x.foo() +16 DEREFERENCE: x.bar +20 val y: A? = x +22 DEREFERENCE: y?.foo() +23 DEREFERENCE: y?.bar diff --git a/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerTest.kt b/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerTest.kt index 0ac64381702..37db8bf7b8a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerTest.kt @@ -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(""), "") } @@ -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) } diff --git a/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestGenerated.java index ce6a4737e23..32e7c0a56f9 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestGenerated.java @@ -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");