diff --git a/idea/resources/inspectionDescriptions/UsePropertyAccessSyntax.html b/idea/resources/inspectionDescriptions/UsePropertyAccessSyntax.html new file mode 100644 index 00000000000..718fb306473 --- /dev/null +++ b/idea/resources/inspectionDescriptions/UsePropertyAccessSyntax.html @@ -0,0 +1,5 @@ + + +This inspection reports calls to java get and set methods that can be replaced with use of Kotlin synthetic properties. + + diff --git a/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/after.kt.template b/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/after.kt.template new file mode 100644 index 00000000000..3351cf05bec --- /dev/null +++ b/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/after.kt.template @@ -0,0 +1,3 @@ +fun foo(thread: Thread) { + thread.daemon = true +} diff --git a/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/before.kt.template b/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/before.kt.template new file mode 100644 index 00000000000..17249916001 --- /dev/null +++ b/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/before.kt.template @@ -0,0 +1,3 @@ +fun foo(thread: Thread) { + thread.setDaemon(true) +} diff --git a/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/description.html b/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/description.html new file mode 100644 index 00000000000..cca81724012 --- /dev/null +++ b/idea/resources/intentionDescriptions/UsePropertyAccessSyntaxIntention/description.html @@ -0,0 +1,5 @@ + + +This inspection replaces calls to java get and set methods with use of Kotlin synthetic properties. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index cc30188b4b7..8894006e50b 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -942,6 +942,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.UsePropertyAccessSyntaxIntention + Kotlin + + + + (UsePropertyAccessSyntaxIntention()) + +class UsePropertyAccessSyntaxIntention : JetSelfTargetingOffsetIndependentIntention(javaClass(), "Use property access syntax") { + override fun isApplicableTo(element: JetCallExpression): Boolean { + if (element.getQualifiedExpressionForSelector()?.getReceiverExpression() is JetSuperExpression) return false // cannot call extensions on "super" + + return findExtensionPropertyToUse(element) != null + } + + override fun applyTo(element: JetCallExpression, editor: Editor) { + val propertyName = findExtensionPropertyToUse(element)!!.getName() + val arguments = element.getValueArguments() + when (arguments.size()) { + 0 -> replaceWithPropertyGet(element, propertyName) + 1 -> replaceWithPropertySet(element, propertyName, arguments.single()) + else -> error("More than one argument in call to accessor") + } + } + + private fun findExtensionPropertyToUse(callExpression: JetCallExpression): PropertyDescriptor? { + val callee = callExpression.getCalleeExpression() as? JetSimpleNameExpression ?: return null + + val bindingContext = callExpression.analyze(BodyResolveMode.PARTIAL) + val resolvedCall = callExpression.getResolvedCall(bindingContext) ?: return null + if (!resolvedCall.getStatus().isSuccess()) return null + + val function = resolvedCall.getResultingDescriptor() as? FunctionDescriptor ?: return null + val resolutionScope = callExpression.getResolutionScope(bindingContext, callExpression.getResolutionFacade()) + val property = findSyntheticProperty(function, resolutionScope) ?: return null + + val moduleDescriptor = callExpression.getResolutionFacade().findModuleDescriptor(callExpression) + val inDescriptor = resolutionScope.getContainingDeclaration() + + fun isVisible(descriptor: DeclarationDescriptor): Boolean { + return if (descriptor is DeclarationDescriptorWithVisibility) + descriptor.isVisible(inDescriptor, bindingContext, callee) + else + true + } + + val referenceVariantsHelper = ReferenceVariantsHelper(bindingContext, moduleDescriptor, callExpression.getProject(), ::isVisible) + val propertyName = property.getName() + val accessibleVariables = referenceVariantsHelper.getReferenceVariants(callee, DescriptorKindFilter.VARIABLES, false, { it == propertyName }) + if (property !in accessibleVariables) return null // shadowed by something else + + return property + } + + private fun findSyntheticProperty(function: FunctionDescriptor, resolutionScope: JetScope): SyntheticExtensionPropertyDescriptor? { + findSyntheticPropertyNoOverriddenCheck(function, resolutionScope)?.let { return it } + + for (overridden in function.getOverriddenDescriptors()) { + findSyntheticProperty(overridden, resolutionScope)?.let { return it } + } + + return null + } + + private fun findSyntheticPropertyNoOverriddenCheck(function: FunctionDescriptor, resolutionScope: JetScope): SyntheticExtensionPropertyDescriptor? { + val owner = function.getContainingDeclaration() + if (owner !is JavaClassDescriptor) return null + + return resolutionScope.getSyntheticExtensionProperties(owner.getDefaultType()) + .filterIsInstance() + .firstOrNull { function == it.getMethod || function == it.setMethod } + } + + private fun replaceWithPropertyGet(callExpression: JetCallExpression, propertyName: Name) { + val newExpression = JetPsiFactory(callExpression).createExpression(propertyName.render()) + callExpression.replace(newExpression) + } + + //TODO: what if it was used as expression (of type Unit)? + private fun replaceWithPropertySet(callExpression: JetCallExpression, propertyName: Name, argument: JetValueArgument) { + val qualifiedExpression = callExpression.getQualifiedExpressionForSelector() + if (qualifiedExpression != null) { + val pattern = when (qualifiedExpression) { + is JetDotQualifiedExpression -> "$0.$1=$2" + is JetSafeQualifiedExpression -> "$0?.$1=$2" + else -> error(qualifiedExpression) //TODO: make it sealed? + } + val newExpression = JetPsiFactory(callExpression).createExpressionByPattern( + pattern, + qualifiedExpression.getReceiverExpression(), + propertyName, + argument.getArgumentExpression()!! + ) + qualifiedExpression.replace(newExpression) + } + else { + val newExpression = JetPsiFactory(callExpression).createExpressionByPattern("$0=$1", propertyName, argument.getArgumentExpression()!!) + callExpression.replace(newExpression) + } + } +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/.intention b/idea/testData/intentions/usePropertyAccessSyntax/.intention new file mode 100644 index 00000000000..07c59e83a91 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.UsePropertyAccessSyntaxIntention diff --git a/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt new file mode 100644 index 00000000000..18d4bff3b10 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt @@ -0,0 +1,8 @@ +// WITH_RUNTIME +import java.io.File + +class MyFile : File("file") + +fun foo(file: MyFile) { + file.getAbsolutePath() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt.after new file mode 100644 index 00000000000..1581334f1b5 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +import java.io.File + +class MyFile : File("file") + +fun foo(file: MyFile) { + file.absolutePath +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt new file mode 100644 index 00000000000..dcec7f3b695 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +import java.io.File + +class MyFile : File("file") { + override fun getCanonicalFile(): File { + return super.getCanonicalFile() + } +} + +fun foo(file: MyFile) { + file.getCanonicalFile() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt.after new file mode 100644 index 00000000000..89f43a93045 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt.after @@ -0,0 +1,12 @@ +// WITH_RUNTIME +import java.io.File + +class MyFile : File("file") { + override fun getCanonicalFile(): File { + return super.getCanonicalFile() + } +} + +fun foo(file: MyFile) { + file.canonicalFile +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/conflict1.kt b/idea/testData/intentions/usePropertyAccessSyntax/conflict1.kt new file mode 100644 index 00000000000..24a31ad2bf1 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/conflict1.kt @@ -0,0 +1,7 @@ +// WITH_RUNTIME +// IS_APPLICABLE: false +import java.io.File + +fun File.foo(absolutePath: String) { + getAbsolutePath() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/conflict2.kt b/idea/testData/intentions/usePropertyAccessSyntax/conflict2.kt new file mode 100644 index 00000000000..872ab35fa8f --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/conflict2.kt @@ -0,0 +1,9 @@ +// WITH_RUNTIME +// IS_APPLICABLE: false +import java.io.File + +val File.absolutePath: String get() = "" + +fun foo(file: File) { + file.getAbsolutePath() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/get.kt b/idea/testData/intentions/usePropertyAccessSyntax/get.kt new file mode 100644 index 00000000000..c675ee0285e --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/get.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import java.io.File + +fun foo(file: File) { + file.getAbsolutePath() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/get.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/get.kt.after new file mode 100644 index 00000000000..f0bfca56602 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/get.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import java.io.File + +fun foo(file: File) { + file.absolutePath +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt b/idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt new file mode 100644 index 00000000000..cccad1ce70e --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import java.io.File + +fun File.foo() { + getAbsolutePath() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt.after new file mode 100644 index 00000000000..1dba46cb7c2 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import java.io.File + +fun File.foo() { + absolutePath +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt b/idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt new file mode 100644 index 00000000000..43cb61ce3f3 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import java.io.File + +fun foo(file: File?) { + file?.getAbsolutePath() +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt.after new file mode 100644 index 00000000000..214787cebcb --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME +import java.io.File + +fun foo(file: File?) { + file?.absolutePath +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/set.kt b/idea/testData/intentions/usePropertyAccessSyntax/set.kt new file mode 100644 index 00000000000..1a5be1d26aa --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/set.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun foo(thread: Thread) { + thread.setName("name") +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/set.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/set.kt.after new file mode 100644 index 00000000000..3a7238cc003 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/set.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun foo(thread: Thread) { + thread.name = "name" +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt b/idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt new file mode 100644 index 00000000000..c35a7e6f944 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun Thread.foo() { + setName("name") +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt.after new file mode 100644 index 00000000000..a639d2fcc57 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun Thread.foo() { + name = "name" +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt b/idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt new file mode 100644 index 00000000000..2c44ee53f9b --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun foo(thread: Thread?) { + thread?.setName("name") +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt.after b/idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt.after new file mode 100644 index 00000000000..15254f25621 --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun foo(thread: Thread?) { + thread?.name = "name" +} \ No newline at end of file diff --git a/idea/testData/intentions/usePropertyAccessSyntax/superCall.kt b/idea/testData/intentions/usePropertyAccessSyntax/superCall.kt new file mode 100644 index 00000000000..a28fb98d88a --- /dev/null +++ b/idea/testData/intentions/usePropertyAccessSyntax/superCall.kt @@ -0,0 +1,9 @@ +// WITH_RUNTIME +// IS_APPLICABLE: false +import java.io.File + +class MyFile : File("file") { + override fun getCanonicalFile(): File { + return super.getCanonicalFile() + } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index f068e1782f7..67130b4b574 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -7382,4 +7382,79 @@ public class IntentionTestGenerated extends AbstractIntentionTest { doTest(fileName); } } + + @TestMetadata("idea/testData/intentions/usePropertyAccessSyntax") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class UsePropertyAccessSyntax extends AbstractIntentionTest { + @TestMetadata("accessThroughKotlinClassInstance.kt") + public void testAccessThroughKotlinClassInstance() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstance.kt"); + doTest(fileName); + } + + @TestMetadata("accessThroughKotlinClassInstanceWithOverride.kt") + public void testAccessThroughKotlinClassInstanceWithOverride() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/accessThroughKotlinClassInstanceWithOverride.kt"); + doTest(fileName); + } + + public void testAllFilesPresentInUsePropertyAccessSyntax() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/usePropertyAccessSyntax"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); + } + + @TestMetadata("conflict1.kt") + public void testConflict1() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/conflict1.kt"); + doTest(fileName); + } + + @TestMetadata("conflict2.kt") + public void testConflict2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/conflict2.kt"); + doTest(fileName); + } + + @TestMetadata("get.kt") + public void testGet() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/get.kt"); + doTest(fileName); + } + + @TestMetadata("getImplicitReceiver.kt") + public void testGetImplicitReceiver() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/getImplicitReceiver.kt"); + doTest(fileName); + } + + @TestMetadata("getSafeCall.kt") + public void testGetSafeCall() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/getSafeCall.kt"); + doTest(fileName); + } + + @TestMetadata("set.kt") + public void testSet() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/set.kt"); + doTest(fileName); + } + + @TestMetadata("setImplicitReceiver.kt") + public void testSetImplicitReceiver() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/setImplicitReceiver.kt"); + doTest(fileName); + } + + @TestMetadata("setSafeCall.kt") + public void testSetSafeCall() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/setSafeCall.kt"); + doTest(fileName); + } + + @TestMetadata("superCall.kt") + public void testSuperCall() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/usePropertyAccessSyntax/superCall.kt"); + doTest(fileName); + } + } }