diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertTryFinallyToUseCallIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertTryFinallyToUseCallIntention.kt index f383821a6cd..65fdde08d73 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertTryFinallyToUseCallIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertTryFinallyToUseCallIntention.kt @@ -17,19 +17,17 @@ package org.jetbrains.kotlin.idea.intentions import com.intellij.openapi.editor.Editor -import com.intellij.psi.PsiElement -import com.intellij.psi.search.LocalSearchScope -import com.intellij.psi.search.searches.ReferencesSearch import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection -import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.allChildren import org.jetbrains.kotlin.psi.psiUtil.contentRange -import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.callUtil.isSafeCall import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver +import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver import org.jetbrains.kotlin.types.typeUtil.supertypes class ConvertTryFinallyToUseCallInspection : IntentionBasedInspection(ConvertTryFinallyToUseCallIntention::class) { @@ -41,17 +39,24 @@ class ConvertTryFinallyToUseCallIntention : SelfTargetingOffsetIndependentIntent ) { override fun applyTo(element: KtTryExpression, editor: Editor?) { val finallySection = element.finallyBlock!! - val finallyDotCall = finallySection.finalExpression.statements.singleOrNull() as KtDotQualifiedExpression - val resourceReference = finallyDotCall.receiverExpression as KtNameReferenceExpression + val finallyExpression = finallySection.finalExpression.statements.single() + val finallyExpressionReceiver = (finallyExpression as? KtDotQualifiedExpression)?.receiverExpression + val resourceReference = finallyExpressionReceiver as? KtNameReferenceExpression + val resourceName = resourceReference?.getReferencedNameAsName() val factory = KtPsiFactory(element) val useCallExpression = factory.buildExpression { - appendName(resourceReference.getReferencedNameAsName()) - appendFixedText(".use {") + if (resourceName != null) { + appendName(resourceName) + appendFixedText(".") + } + appendFixedText("use {") - appendName(resourceReference.getReferencedNameAsName()) - appendFixedText("->") + if (resourceName != null) { + appendName(resourceName) + appendFixedText("->") + } appendChildRange(element.tryBlock.contentRange()) appendFixedText("}") @@ -63,21 +68,33 @@ class ConvertTryFinallyToUseCallIntention : SelfTargetingOffsetIndependentIntent override fun isApplicableTo(element: KtTryExpression): Boolean { // Single statement in finally, no catch blocks val finallySection = element.finallyBlock ?: return false - val finallyDotCall = finallySection.finalExpression.statements.singleOrNull() as? KtDotQualifiedExpression ?: return false + val finallyExpression = finallySection.finalExpression.statements.singleOrNull() ?: return false if (element.catchClauses.isNotEmpty()) return false - // Like resource.close() - val resourceReference = finallyDotCall.receiverExpression as? KtNameReferenceExpression ?: return false - val resourceCall = finallyDotCall.selectorExpression as? KtCallExpression ?: return false - if (resourceCall.calleeExpression?.text != "close") return false - - // resource is Closeable immutable local variable - val resourceDescriptor = - element.analyze().get(BindingContext.REFERENCE_TARGET, resourceReference) as? VariableDescriptor ?: return false - return !resourceDescriptor.isVar && resourceDescriptor.type.supertypes().any { + val context = element.analyze() + val resolvedCall = finallyExpression.getResolvedCall(context) ?: return false + if (resolvedCall.call.isSafeCall()) return false + if (resolvedCall.candidateDescriptor.name.asString() != "close") return false + val receiver = resolvedCall.dispatchReceiver ?: return false + if (receiver.type.supertypes().all { it.constructor.declarationDescriptor?.fqNameSafe?.asString().let { - it == "java.io.Closeable" || it == "java.lang.AutoCloseable" + it != "java.io.Closeable" && it != "java.lang.AutoCloseable" } + }) return false + + return when (receiver) { + is ExpressionReceiver -> { + val expression = receiver.expression + if (expression is KtThisExpression) true + else { + val resourceReference = expression as? KtReferenceExpression ?: return false + val resourceDescriptor = + context[BindingContext.REFERENCE_TARGET, resourceReference] as? VariableDescriptor ?: return false + !resourceDescriptor.isVar + } + } + is ImplicitReceiver -> true + else -> false } } } \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt b/idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt new file mode 100644 index 00000000000..1ad4cc0f859 --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun BufferedReader.foo() { + try { + readLine() + } + finally { + close() + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt.after b/idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt.after new file mode 100644 index 00000000000..cc1c75470fc --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt.after @@ -0,0 +1,7 @@ +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun BufferedReader.foo() { + use { readLine() } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt b/idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt new file mode 100644 index 00000000000..d1836d23e17 --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun foo(reader: BufferedReader) { + try { + reader.readLine() + } + finally { + reader.close() + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt.after b/idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt.after new file mode 100644 index 00000000000..0f0d8d2c1dd --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt.after @@ -0,0 +1,7 @@ +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun foo(reader: BufferedReader) { + reader.use { reader -> reader.readLine() } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/safeCall.kt b/idea/testData/intentions/convertTryFinallyToUseCall/safeCall.kt new file mode 100644 index 00000000000..dd7664e5953 --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/safeCall.kt @@ -0,0 +1,13 @@ +// IS_APPLICABLE: false +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun foo(reader: BufferedReader?) { + try { + reader?.readLine() + } + finally { + reader?.close() + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/this.kt b/idea/testData/intentions/convertTryFinallyToUseCall/this.kt new file mode 100644 index 00000000000..42b4efd1a72 --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/this.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun BufferedReader.foo() { + try { + this.readLine() + } + finally { + this.close() + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/this.kt.after b/idea/testData/intentions/convertTryFinallyToUseCall/this.kt.after new file mode 100644 index 00000000000..d71c6bd23ab --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/this.kt.after @@ -0,0 +1,7 @@ +// WITH_RUNTIME +import java.io.File +import java.io.BufferedReader + +fun BufferedReader.foo() { + use { this.readLine() } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt b/idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt new file mode 100644 index 00000000000..c6cde4884cc --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt @@ -0,0 +1,18 @@ +// WITH_RUNTIME + +import java.io.Closeable + +class MyCloseable : Closeable { + override fun close() {} + + fun process(x: Int) = x + + fun Int.foo() { + try { + this@MyCloseable.process(this) + } + finally { + this@MyCloseable.close() + } + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt.after b/idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt.after new file mode 100644 index 00000000000..4b631e38559 --- /dev/null +++ b/idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt.after @@ -0,0 +1,13 @@ +// WITH_RUNTIME + +import java.io.Closeable + +class MyCloseable : Closeable { + override fun close() {} + + fun process(x: Int) = x + + fun Int.foo() { + use { this@MyCloseable.process(this) } + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index de504b0ee01..485531ecc56 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -6108,6 +6108,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest { doTest(fileName); } + @TestMetadata("implicitReceiver.kt") + public void testImplicitReceiver() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/implicitReceiver.kt"); + doTest(fileName); + } + @TestMetadata("notClose.kt") public void testNotClose() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/notClose.kt"); @@ -6120,12 +6126,36 @@ public class IntentionTestGenerated extends AbstractIntentionTest { doTest(fileName); } + @TestMetadata("parameter.kt") + public void testParameter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/parameter.kt"); + doTest(fileName); + } + + @TestMetadata("safeCall.kt") + public void testSafeCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/safeCall.kt"); + doTest(fileName); + } + @TestMetadata("simple.kt") public void testSimple() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/simple.kt"); doTest(fileName); } + @TestMetadata("this.kt") + public void testThis() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/this.kt"); + doTest(fileName); + } + + @TestMetadata("thisLabeled.kt") + public void testThisLabeled() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/thisLabeled.kt"); + doTest(fileName); + } + @TestMetadata("triple.kt") public void testTriple() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertTryFinallyToUseCall/triple.kt");