From 2f19713cc268388813eb6698774424ec763bff7b Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Fri, 21 Nov 2014 01:50:45 +0300 Subject: [PATCH] Partial body resolve: rewritten algorithm of filter building to ensure correct resolve of smart cast points (+ prepared to support more strict mode for resolve only) --- .../resolve/lazy/PartialBodyResolveFilter.kt | 255 +++++++++++++----- .../partialBodyResolve/MutliDeclaration.dump | 16 ++ .../partialBodyResolve/MutliDeclaration.kt | 14 + .../SmartCastInTheSameStatement.dump | 21 ++ .../SmartCastInTheSameStatement.kt | 19 ++ .../SmartCastPointsResolveRequired1.dump | 17 ++ ...odo => SmartCastPointsResolveRequired1.kt} | 0 .../SmartCastPointsResolveRequired2.dump | 12 + ...odo => SmartCastPointsResolveRequired2.kt} | 0 .../PartialBodyResolveTestGenerated.java | 24 ++ 10 files changed, 315 insertions(+), 63 deletions(-) create mode 100644 idea/testData/resolve/partialBodyResolve/MutliDeclaration.dump create mode 100644 idea/testData/resolve/partialBodyResolve/MutliDeclaration.kt create mode 100644 idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.dump create mode 100644 idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.kt create mode 100644 idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.dump rename idea/testData/resolve/partialBodyResolve/{SmartCastPointsResolveRequired1.kt.todo => SmartCastPointsResolveRequired1.kt} (100%) create mode 100644 idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.dump rename idea/testData/resolve/partialBodyResolve/{SmartCastPointsResolveRequired2.kt.todo => SmartCastPointsResolveRequired2.kt} (100%) diff --git a/idea/ide-common/src/org/jetbrains/jet/lang/resolve/lazy/PartialBodyResolveFilter.kt b/idea/ide-common/src/org/jetbrains/jet/lang/resolve/lazy/PartialBodyResolveFilter.kt index 5822e4d5dc5..ff6f344380a 100644 --- a/idea/ide-common/src/org/jetbrains/jet/lang/resolve/lazy/PartialBodyResolveFilter.kt +++ b/idea/ide-common/src/org/jetbrains/jet/lang/resolve/lazy/PartialBodyResolveFilter.kt @@ -37,7 +37,7 @@ class PartialBodyResolveFilter( probablyNothingCallableNamesService: ProbablyNothingCallableNamesService ) : (JetElement) -> Boolean { - private val statementsToResolve = HashSet() + private val statementTree = StatementTree() private val nothingFunctionNames = HashSet(probablyNothingCallableNamesService.functionNames()) private val nothingPropertyNames = probablyNothingCallableNamesService.propertyNames() @@ -59,76 +59,95 @@ class PartialBodyResolveFilter( } }) - addStatementsToResolve(elementToResolve) + statementTree.mark(elementToResolve, MarkLevel.NEED_COMPLETION) + statementTree.blocks(body).forEach { + processBlock(it) + } } - override fun invoke(statement: JetElement) = statement in statementsToResolve + //TODO: do..while is special case - tailRecursive - private fun addStatementsToResolve(element: JetElement) { - if (element == body) return - val parent = element.getParent() as? JetElement ?: return + private fun processBlock(block: JetBlockExpression): NameFilter { + val nameFilter = NameFilter() + if (!statementTree.hasMarks(block, MarkLevel.NEED_REFERENCE_RESOLVE)) return nameFilter - if (parent is JetBlockExpression) { - if (element in statementsToResolve) return // already processed + val startStatement = block.getLastChild().siblings(forward = false) + .filterIsInstance() + .first { statementTree.statementMark(it) > MarkLevel.RESOLVE_STATEMENT } - if (element is JetExpression) { - addStatementToResolve(element) + for (statement in startStatement.siblings(forward = false)) { + if (statement !is JetExpression) continue + + if (statement is JetNamedDeclaration) { + val name = statement.getName() + if (name != null && nameFilter.accepts(name)) { + statementTree.mark(statement, MarkLevel.NEED_REFERENCE_RESOLVE) + } + } + else if (statement is JetMultiDeclaration) { + if (statement.getEntries().any { + val name = it.getName() + name != null && nameFilter.accepts(name) + }) { + statementTree.mark(statement, MarkLevel.NEED_REFERENCE_RESOLVE) + } } - for (statement in element.siblings(forward = false, withItself = false)) { - if (statement !is JetExpression) continue - if (statement is JetClassBody) continue - - val smartCastPlaces = potentialSmartCastPlaces(statement) - if (!smartCastPlaces.isEmpty()) { - addStatementToResolve(statement) - //TODO: do we really need correct resolve for ALL smart cast places? - addStatementsToResolveForPlaces(smartCastPlaces.values().flatMap { it }) + fun updateNameFilter() { + val level = statementTree.statementMark(statement) + when (level) { + MarkLevel.NEED_REFERENCE_RESOLVE -> nameFilter.addUsedNames(statement) + MarkLevel.NEED_COMPLETION -> nameFilter.addAllNames() } - else if (statement is JetDeclaration) { - addStatementToResolve(statement) + } + + updateNameFilter() + + if (!nameFilter.isEmpty) { + val smartCastPlaces = potentialSmartCastPlaces(statement, { it.identifiers().all { nameFilter.accepts(it) } }) + if (!smartCastPlaces.isEmpty()) { + //TODO: do we really need correct resolve for ALL smart cast places? + smartCastPlaces.values() + .flatMap { it } + .forEach { statementTree.mark(it, MarkLevel.NEED_REFERENCE_RESOLVE) } + updateNameFilter() + } + } + + val level = statementTree.statementMark(statement) + if (level > MarkLevel.RESOLVE_STATEMENT) { + for (nestedBlock in statementTree.blocks(statement)) { + if (isValueNeeded(nestedBlock)) { + nestedBlock.lastStatement()?.let { statementTree.mark(it, MarkLevel.NEED_REFERENCE_RESOLVE) } + } + val childFilter = processBlock(nestedBlock) + nameFilter.addNames(childFilter) } } } - addStatementsToResolve(parent) + return nameFilter } - private fun addStatementToResolve(statement: JetExpression): Boolean { - assert(statement.getParent() is JetBlockExpression) - if (!statementsToResolve.add(statement)) return false - // search for code blocks inside statement and make sure its values will be resolved - statement.accept(object : JetVisitorVoid(){ - override fun visitBlockExpression(expression: JetBlockExpression) { - if (isValueNeeded(expression)) { - val value = expression.lastStatement() - if (value != null) { - addStatementsToResolve(value) - } - } - } - - override fun visitJetElement(element: JetElement) { - element.acceptChildren(this) - } - }) - return true - } + override fun invoke(statement: JetElement) + = statement is JetExpression && statementTree.statementMark(statement) != MarkLevel.NONE /** * Finds places within the given statement that may affect smart-casts after it. * That is, such places whose containing statements must be left in code to keep the smart-casts. * Returns map from smart-cast expression names (variable name or qualified variable name) to places. */ - private fun potentialSmartCastPlaces(statement: JetExpression, filter: (String) -> Boolean = { true }): Map> { - val map = HashMap>(0) + private fun potentialSmartCastPlaces( + statement: JetExpression, + filter: (SmartCastName) -> Boolean = { true } + ): Map> { + val map = HashMap>(0) - fun addPlace(name: String, place: JetExpression) { + fun addPlace(name: SmartCastName, place: JetExpression) { map.getOrPut(name, { ArrayList(1) }).add(place) } - fun addPlaces(name: String, places: Collection) { + fun addPlaces(name: SmartCastName, places: Collection) { assert(!places.isEmpty()) map.getOrPut(name, { ArrayList(places.size) }).addAll(places) } @@ -219,8 +238,8 @@ class PartialBodyResolveFilter( * Returns names of expressions that would possibly be smart cast after * either a statement "if (condition) return" or "if (!condition) return" */ - private fun collectPossiblySmartCastInCondition(condition: JetExpression?): Set { - val result = HashSet() + private fun collectPossiblySmartCastInCondition(condition: JetExpression?): Set { + val result = HashSet() condition?.accept(object : ControlFlowVisitor() { override fun visitBinaryExpression(expression: JetBinaryExpression) { expression.acceptChildren(this) @@ -352,28 +371,33 @@ class PartialBodyResolveFilter( private fun JetElement.noControlFlowInside() = this is JetFunction || this is JetClass || this is JetClassBody } - private fun addStatementsToResolveForPlaces(places: Collection) { - for (place in places) { - var parent: PsiElement = place - while (true) { - if (parent.isStatement() && !addStatementToResolve(parent as JetExpression)) break - parent = parent.getParent() - } + private fun PsiElement.isStatement() = this is JetExpression && getParent() is JetBlockExpression + + private data class SmartCastName(val receiverName: SmartCastName?, val selectorName: String) { + override fun toString(): String = if (receiverName != null) receiverName.toString() + "." + selectorName else selectorName + + fun identifiers(): Collection { + return if (receiverName != null) + receiverName.identifiers() + listOf(selectorName) + else + listOf(selectorName) } } - private fun PsiElement.isStatement() = this is JetExpression && getParent() is JetBlockExpression - - private fun JetExpression.smartCastExpressionName(): String? { + //TODO: this can be smart-cast too! + private fun JetExpression.smartCastExpressionName(): SmartCastName? { return when (this) { - is JetSimpleNameExpression -> this.getReferencedName() + is JetSimpleNameExpression -> SmartCastName(null, this.getReferencedName()) is JetQualifiedExpression -> { - val selectorName = getSelectorExpression().smartCastExpressionName() ?: return null + val selector = getSelectorExpression() as? JetSimpleNameExpression ?: return null + val selectorName = selector.getReferencedName() val receiver = getReceiverExpression() - if (receiver is JetThisExpression) return selectorName + if (receiver is JetThisExpression) { + return SmartCastName(null, selectorName) + } val receiverName = receiver.smartCastExpressionName() ?: return null - return selectorName + "." + receiverName + return SmartCastName(receiverName, selectorName) } else -> null @@ -409,5 +433,110 @@ class PartialBodyResolveFilter( private fun JetBlockExpression.lastStatement(): JetExpression? = getLastChild()?.siblings(forward = false)?.firstOrNullIsInstance() + + //TODO: declarations with special names (e.g. "get") + private class NameFilter { + private var names: MutableSet? = HashSet() + + fun accepts(name: String) = names == null || name in names!! + + val isEmpty: Boolean + get() = names?.isEmpty() ?: false + + private val addUsedNamesVisitor = object : JetVisitorVoid(){ + override fun visitSimpleNameExpression(expression: JetSimpleNameExpression) { + names!!.add(expression.getReferencedName()) + } + + override fun visitJetElement(element: JetElement) { + element.acceptChildren(this) + } + + override fun visitBlockExpression(expression: JetBlockExpression) { + } + } + + fun addUsedNames(statement: JetExpression) { + if (names != null) { + statement.accept(addUsedNamesVisitor) + } + } + + fun addNames(filter: NameFilter) { + if (names == null) return + if (filter.names == null) { + names = null + } + else { + names!!.addAll(filter.names!!) + } + } + + fun addAllNames() { + names = null + } + } + + private enum class MarkLevel { + NONE + RESOLVE_STATEMENT + NEED_REFERENCE_RESOLVE + NEED_COMPLETION + } + + private inner class StatementTree { + private val statementMarks = HashMap() + private val blockLevels = HashMap() + + val topLevelBlocks: Collection = blocks(body) + + fun blocks(statement: JetExpression): Collection + = blocks(statement : JetElement) + + private fun blocks(element: JetElement): Collection { + val result = ArrayList(1) + element.accept(object : JetVisitorVoid() { + override fun visitBlockExpression(expression: JetBlockExpression) { + result.add(expression) + } + + override fun visitElement(element: PsiElement) { + element.acceptChildren(this) + } + }) + return result + } + + fun mark(element: PsiElement, level: MarkLevel) { + var e = element + while (e != body) { + if (e.isStatement()) { + markStatement(e as JetExpression, level) + } + e = e.getParent()!! + } + } + + private fun markStatement(statement: JetExpression, level: MarkLevel) { + val currentLevel = statementMark(statement) + if (currentLevel < level) { + statementMarks[statement] = level + + val block = statement.getParent() as JetBlockExpression + val currentBlockLevel = blockLevels[block] ?: MarkLevel.NONE + if (currentBlockLevel < level) { + blockLevels[block] = level + } + } + } + + fun statementMark(statement: JetExpression): MarkLevel + = statementMarks[statement] ?: MarkLevel.NONE + + fun hasMarks(block: JetBlockExpression, minLevel: MarkLevel): Boolean { + val level = blockLevels[block] ?: return minLevel == MarkLevel.NONE + return level >= minLevel + } + } } diff --git a/idea/testData/resolve/partialBodyResolve/MutliDeclaration.dump b/idea/testData/resolve/partialBodyResolve/MutliDeclaration.dump new file mode 100644 index 00000000000..c280f229666 --- /dev/null +++ b/idea/testData/resolve/partialBodyResolve/MutliDeclaration.dump @@ -0,0 +1,16 @@ +Resolve target: value-parameter val b: kotlin.String? +---------------------------------------------- +data class TestData(val first: Int, val second: String?) + +fun f() : TestData {} + +fun test(b: String?, p: Int) { + if (p > 0) { + val (a, b) = f() + print(b!!.size) + } + else { + print(b!!.size) + } + b +} diff --git a/idea/testData/resolve/partialBodyResolve/MutliDeclaration.kt b/idea/testData/resolve/partialBodyResolve/MutliDeclaration.kt new file mode 100644 index 00000000000..2202253e268 --- /dev/null +++ b/idea/testData/resolve/partialBodyResolve/MutliDeclaration.kt @@ -0,0 +1,14 @@ +data class TestData(val first: Int, val second: String?) + +fun f() : TestData {} + +fun test(b: String?, p: Int) { + if (p > 0) { + val (a, b) = f() + print(b!!.size) + } + else { + print(b!!.size) + } + b +} diff --git a/idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.dump b/idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.dump new file mode 100644 index 00000000000..f2ae2fa5187 --- /dev/null +++ b/idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.dump @@ -0,0 +1,21 @@ +Resolve target: value-parameter val s: kotlin.String? +---------------------------------------------- +fun foo(s: String?, b1: Boolean, b2: Boolean) { + x( + if (b1) { + print(s!!.length) + 1 + } else { + if (b2) { + print(s!!.length + 1) + } + else { + print(s!!.length - 1) + } + 2 + }, + s + ) +} + +fun x(p1: Int, p2: String) { } \ No newline at end of file diff --git a/idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.kt b/idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.kt new file mode 100644 index 00000000000..2f87e84e3cf --- /dev/null +++ b/idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.kt @@ -0,0 +1,19 @@ +fun foo(s: String?, b1: Boolean, b2: Boolean) { + x( + if (b1) { + print(s!!.length) + 1 + } else { + if (b2) { + print(s!!.length + 1) + } + else { + print(s!!.length - 1) + } + 2 + }, + s + ) +} + +fun x(p1: Int, p2: String) { } \ No newline at end of file diff --git a/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.dump b/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.dump new file mode 100644 index 00000000000..27cc477451d --- /dev/null +++ b/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.dump @@ -0,0 +1,17 @@ +Resolve target: value-parameter val x: kotlin.Any? +---------------------------------------------- +fun foo(x: Any?, p: Int) { + if (p > 0) { + val x = f() + print(x!!) + } + else { + val x = g() + print(x!!) + } + + x.hashCode() +} + +fun f(): Any? = null +fun g(): Any? = null diff --git a/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.kt.todo b/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.kt similarity index 100% rename from idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.kt.todo rename to idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.kt diff --git a/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.dump b/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.dump new file mode 100644 index 00000000000..79489705b90 --- /dev/null +++ b/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.dump @@ -0,0 +1,12 @@ +Resolve target: value-parameter val x: kotlin.Any? +---------------------------------------------- +fun foo(x: Any?) { + if (x == null) { + val error = f() + error("error") + } + + x.hashCode() +} + +fun f(): (String) -> Unit = {} diff --git a/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.kt.todo b/idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.kt similarity index 100% rename from idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.kt.todo rename to idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.kt diff --git a/idea/tests/org/jetbrains/jet/resolve/PartialBodyResolveTestGenerated.java b/idea/tests/org/jetbrains/jet/resolve/PartialBodyResolveTestGenerated.java index 25a3b2424dd..53610227506 100644 --- a/idea/tests/org/jetbrains/jet/resolve/PartialBodyResolveTestGenerated.java +++ b/idea/tests/org/jetbrains/jet/resolve/PartialBodyResolveTestGenerated.java @@ -264,6 +264,12 @@ public class PartialBodyResolveTestGenerated extends AbstractPartialBodyResolveT doTest(fileName); } + @TestMetadata("MutliDeclaration.kt") + public void testMutliDeclaration() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/resolve/partialBodyResolve/MutliDeclaration.kt"); + doTest(fileName); + } + @TestMetadata("ReturnLambda.kt") public void testReturnLambda() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/resolve/partialBodyResolve/ReturnLambda.kt"); @@ -276,6 +282,24 @@ public class PartialBodyResolveTestGenerated extends AbstractPartialBodyResolveT doTest(fileName); } + @TestMetadata("SmartCastInTheSameStatement.kt") + public void testSmartCastInTheSameStatement() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/resolve/partialBodyResolve/SmartCastInTheSameStatement.kt"); + doTest(fileName); + } + + @TestMetadata("SmartCastPointsResolveRequired1.kt") + public void testSmartCastPointsResolveRequired1() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired1.kt"); + doTest(fileName); + } + + @TestMetadata("SmartCastPointsResolveRequired2.kt") + public void testSmartCastPointsResolveRequired2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/resolve/partialBodyResolve/SmartCastPointsResolveRequired2.kt"); + doTest(fileName); + } + @TestMetadata("ThisQualifiedAutoCast.kt") public void testThisQualifiedAutoCast() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/resolve/partialBodyResolve/ThisQualifiedAutoCast.kt");