From a0868635617b07fa298b77bee55db6369bda7ee1 Mon Sep 17 00:00:00 2001 From: Mikhail Glukhikh Date: Thu, 10 Aug 2017 17:58:48 +0300 Subject: [PATCH] Preliminary: support smart casts on variables in closures No feature support yet So #KT-14486 In Progress --- .../calls/smartcasts/DataFlowValueFactory.kt | 44 ++++++++++++-- .../expressions/AssignedVariablesSearcher.kt | 8 ++- .../PreliminaryDeclarationVisitor.kt | 2 +- .../capturedInClosureModifiedBefore.kt | 60 +++++++++++++++++++ .../capturedInClosureModifiedBefore.txt | 8 +++ .../varnotnull/capturedInClosureOff.kt | 15 +++++ .../varnotnull/capturedInClosureOff.txt | 4 ++ .../checkers/DiagnosticsTestGenerated.java | 12 ++++ .../kotlin/config/LanguageVersionSettings.kt | 1 + 9 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.txt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.txt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt index fd6fc543578..26cef648be0 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt @@ -40,6 +40,7 @@ import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver import org.jetbrains.kotlin.resolve.scopes.receivers.ReceiverValue import org.jetbrains.kotlin.resolve.scopes.receivers.TransientReceiver import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.expressions.AssignedVariablesSearcher.Writer import org.jetbrains.kotlin.types.expressions.ExpressionTypingUtils import org.jetbrains.kotlin.types.expressions.PreliminaryDeclarationVisitor import org.jetbrains.kotlin.types.isError @@ -296,18 +297,40 @@ object DataFlowValueFactory { false } + private fun hasNoWritersInClosures( + variableContainingDeclaration: DeclarationDescriptor, + writers: Set, + bindingContext: BindingContext + ): Boolean { + return writers.none { (_, writerDeclaration) -> + val writerDescriptor = writerDeclaration?.let { + ControlFlowInformationProvider.getDeclarationDescriptorIncludingConstructors(bindingContext, it) + } + writerDeclaration != null && variableContainingDeclaration != writerDescriptor + } + } + + private fun isAccessedInsideClosureAfterAllWriters( + writers: Set, + accessElement: KtElement + ): Boolean { + val parent = ControlFlowInformationProvider.getElementParentDeclaration(accessElement) ?: return false + return writers.none { (assignment) -> !assignment.before(parent) } + } + private fun isAccessedBeforeAllClosureWriters( variableContainingDeclaration: DeclarationDescriptor, - writers: Set, + writers: Set, bindingContext: BindingContext, accessElement: KtElement ): Boolean { // All writers should be before access element, with the exception: // writer which is the same with declaration site does not count - writers.filterNotNull().forEach { writer -> - val writerDescriptor = ControlFlowInformationProvider.getDeclarationDescriptorIncludingConstructors(bindingContext, writer) - // Access is after some writer - if (variableContainingDeclaration != writerDescriptor && !accessElement.before(writer)) { + writers.mapNotNull { it.declaration }.forEach { writerDeclaration -> + val writerDescriptor = ControlFlowInformationProvider.getDeclarationDescriptorIncludingConstructors( + bindingContext, writerDeclaration) + // Access is after some writerDeclaration + if (variableContainingDeclaration != writerDescriptor && !accessElement.before(writerDeclaration)) { return false } } @@ -353,7 +376,16 @@ object DataFlowValueFactory { // If access element is inside closure: captured val variableContainingDeclaration = variableDescriptor.containingDeclaration - if (isAccessedInsideClosure(variableContainingDeclaration, bindingContext, accessElement)) return CAPTURED_VARIABLE + if (isAccessedInsideClosure(variableContainingDeclaration, bindingContext, accessElement)) { + // stable iff we have no writers in closures AND this closure is AFTER all writers + return if (hasNoWritersInClosures(variableContainingDeclaration, writers, bindingContext) && + isAccessedInsideClosureAfterAllWriters(writers, accessElement)) { + STABLE_VARIABLE + } + else { + CAPTURED_VARIABLE + } + } // Otherwise, stable iff considered position is BEFORE all writers except declarer itself return if (isAccessedBeforeAllClosureWriters(variableContainingDeclaration, writers, bindingContext, accessElement)) diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/AssignedVariablesSearcher.kt b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/AssignedVariablesSearcher.kt index caf034738eb..68baa0c661b 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/AssignedVariablesSearcher.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/AssignedVariablesSearcher.kt @@ -25,9 +25,11 @@ import org.jetbrains.kotlin.psi.* abstract class AssignedVariablesSearcher: KtTreeVisitorVoid() { - private val assignedNames: SetMultimap = LinkedHashMultimap.create() + data class Writer(val assignment: KtBinaryExpression, val declaration: KtDeclaration?) - open fun writers(variableDescriptor: VariableDescriptor): MutableSet = assignedNames[variableDescriptor.name] + private val assignedNames: SetMultimap = LinkedHashMultimap.create() + + open fun writers(variableDescriptor: VariableDescriptor): MutableSet = assignedNames[variableDescriptor.name] fun hasWriters(variableDescriptor: VariableDescriptor) = writers(variableDescriptor).isNotEmpty() @@ -53,7 +55,7 @@ abstract class AssignedVariablesSearcher: KtTreeVisitorVoid() { if (binaryExpression.operationToken === KtTokens.EQ) { val left = KtPsiUtil.deparenthesize(binaryExpression.left) if (left is KtNameReferenceExpression) { - assignedNames.put(left.getReferencedNameAsName(), currentDeclaration) + assignedNames.put(left.getReferencedNameAsName(), Writer(binaryExpression, currentDeclaration)) } } super.visitBinaryExpression(binaryExpression) diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PreliminaryDeclarationVisitor.kt b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PreliminaryDeclarationVisitor.kt index 1979448a56e..3470670759c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PreliminaryDeclarationVisitor.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PreliminaryDeclarationVisitor.kt @@ -28,7 +28,7 @@ import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils class PreliminaryDeclarationVisitor(val declaration: KtDeclaration): AssignedVariablesSearcher() { - override fun writers(variableDescriptor: VariableDescriptor): MutableSet { + override fun writers(variableDescriptor: VariableDescriptor): MutableSet { lazyTrigger return super.writers(variableDescriptor) } diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.kt new file mode 100644 index 00000000000..f440185405f --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.kt @@ -0,0 +1,60 @@ +// !LANGUAGE: +CapturedInClosureSmartCasts + +fun run(f: () -> Unit) = f() + +fun foo(s: String?) { + var x: String? = null + if (s != null) { + x = s + } + if (x != null) { + run { + x.hashCode() + } + } +} + +fun bar(s: String?) { + var x = s + if (x != null) { + run { + x.hashCode() + } + } +} + +fun baz(s: String?) { + var x = s + if (x != null) { + run { + x.hashCode() + } + run { + x.hashCode() + x = null + } + } +} + +fun gaz(s: String?) { + var x = s + if (x != null) { + run { + x.hashCode() + x = null + } + run { + x.hashCode() + } + } +} + +fun gav(s: String?) { + var x = s + if (x != null) { + run { + x.hashCode() + } + x = null + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.txt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.txt new file mode 100644 index 00000000000..9ebcf178bf9 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.txt @@ -0,0 +1,8 @@ +package + +public fun bar(/*0*/ s: kotlin.String?): kotlin.Unit +public fun baz(/*0*/ s: kotlin.String?): kotlin.Unit +public fun foo(/*0*/ s: kotlin.String?): kotlin.Unit +public fun gav(/*0*/ s: kotlin.String?): kotlin.Unit +public fun gaz(/*0*/ s: kotlin.String?): kotlin.Unit +public fun run(/*0*/ f: () -> kotlin.Unit): kotlin.Unit diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.kt new file mode 100644 index 00000000000..45f39b95079 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.kt @@ -0,0 +1,15 @@ +// !LANGUAGE: -CapturedInClosureSmartCasts + +fun run(f: () -> Unit) = f() + +fun foo(s: String?) { + var x: String? = null + if (s != null) { + x = s + } + if (x != null) { + run { + x.hashCode() + } + } +} diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.txt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.txt new file mode 100644 index 00000000000..7f36b02f85d --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.txt @@ -0,0 +1,4 @@ +package + +public fun foo(/*0*/ s: kotlin.String?): kotlin.Unit +public fun run(/*0*/ f: () -> kotlin.Unit): kotlin.Unit diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java index 004e724ad1d..6dbc233beb9 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java @@ -21309,6 +21309,18 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest { doTest(fileName); } + @TestMetadata("capturedInClosureModifiedBefore.kt") + public void testCapturedInClosureModifiedBefore() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.kt"); + doTest(fileName); + } + + @TestMetadata("capturedInClosureOff.kt") + public void testCapturedInClosureOff() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureOff.kt"); + doTest(fileName); + } + @TestMetadata("doWhileWithBreak.kt") public void testDoWhileWithBreak() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/varnotnull/doWhileWithBreak.kt"); diff --git a/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt b/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt index 6d130ebf21f..b21ab4d982d 100644 --- a/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt +++ b/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt @@ -56,6 +56,7 @@ enum class LanguageFeature( DeprecatedFieldForInvisibleCompanionObject(KOTLIN_1_2), SafeCastCheckBoundSmartCasts(KOTLIN_1_2), BooleanElvisBoundSmartCasts(KOTLIN_1_2), + CapturedInClosureSmartCasts(KOTLIN_1_2), // Experimental features