Add inspection + intention to replace .let { it.foo() } with .foo() #KT-13551 Fixed

This commit is contained in:
shiraji
2016-09-05 23:59:41 +03:00
committed by Mikhail Glukhikh
parent cdf4309e04
commit e4c873dc6a
32 changed files with 452 additions and 0 deletions
@@ -0,0 +1,5 @@
<html>
<body>
This inspection detects redundant '.let', when it includes only one call with lambda parameter as receiver.
</body>
</html>
@@ -0,0 +1 @@
text?.<spot>length</spot>
@@ -0,0 +1 @@
text?.let <spot>{ it.length }</spot>
@@ -0,0 +1,5 @@
<html>
<body>
This intention removes redundant '.let', when it includes only one call with lambda parameter as receiver.
</body>
</html>
+13
View File
@@ -1348,6 +1348,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ReplaceSingleLineLetIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaInspection"
displayName="Object literal can be converted to lambda"
groupName="Kotlin"
@@ -1773,6 +1778,14 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ReplaceSingleLineLetInspection"
displayName="Replace single line .let"
groupName="Kotlin"
enabledByDefault="true"
level="WARNING"
language="kotlin"
/>
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
@@ -0,0 +1,121 @@
/*
* Copyright 2010-2016 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.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.lexer.KtToken
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getStartOffsetIn
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
class ReplaceSingleLineLetInspection : IntentionBasedInspection<KtCallExpression>(ReplaceSingleLineLetIntention::class) {
override fun inspectionRange(element: KtCallExpression) = element.calleeExpression?.let {
val start = it.getStartOffsetIn(element)
TextRange(start, start + it.endOffset - it.startOffset)
}
}
class ReplaceSingleLineLetIntention : SelfTargetingOffsetIndependentIntention<KtCallExpression>(
KtCallExpression::class.java,
"Remove redundant '.let' call"
) {
override fun applyTo(element: KtCallExpression, editor: Editor?) {
val lambdaExpression = element.lambdaArguments.firstOrNull()?.getLambdaExpression() ?: return
val bodyExpression = lambdaExpression.bodyExpression?.children?.singleOrNull() ?: return
val dotQualifiedExpression = bodyExpression as? KtDotQualifiedExpression ?: return
val parent = element.parent
when (parent) {
is KtQualifiedExpression -> {
val factory = KtPsiFactory(element.project)
val receiver = parent.receiverExpression
parent.replace(dotQualifiedExpression.replaceFirstReceiver(factory, receiver, parent.operationSign == KtTokens.SAFE_ACCESS))
}
else -> {
element.replace(dotQualifiedExpression.deleteFirstReceiver())
}
}
}
private fun KtDotQualifiedExpression.deleteFirstReceiver(): KtExpression {
val receiver = receiverExpression
when (receiver) {
is KtDotQualifiedExpression -> receiver.deleteFirstReceiver()
else -> selectorExpression?.let { return this.replace(it) as KtExpression }
}
return this
}
private fun KtDotQualifiedExpression.replaceFirstReceiver(
factory: KtPsiFactory,
newReceiver: KtExpression,
safeAccess: Boolean = false
): KtExpression {
val receiver = receiverExpression
when (receiver) {
is KtDotQualifiedExpression -> {
receiver.replaceFirstReceiver(factory, newReceiver, safeAccess)
}
else -> {
if (safeAccess) {
operationTokenNode.psi.replace(factory.createSafeCallNode().psi)
}
receiver.replace(newReceiver)
}
}
return this
}
override fun isApplicableTo(element: KtCallExpression): Boolean {
if (!isLetMethod(element)) return false
val lambdaExpression = element.lambdaArguments.firstOrNull()?.getLambdaExpression() ?: return false
val bodyExpression = lambdaExpression.bodyExpression?.children?.singleOrNull() ?: return false
val dotQualifiedExpression = bodyExpression as? KtDotQualifiedExpression ?: return false
val parameterName = lambdaExpression.getParameterName() ?: return false
val receiverExpression = dotQualifiedExpression.getLeftMostReceiverExpression()
if (receiverExpression.text != parameterName) return false
return !dotQualifiedExpression.receiverUsedAsArgument(parameterName)
}
private fun isLetMethod(element: KtCallExpression) =
element.calleeExpression?.text == "let" && isMethodCall(element, "kotlin.let")
private fun isMethodCall(expression: KtExpression, fqMethodName: String): Boolean {
val resolvedCall = expression.getResolvedCall(expression.analyze()) ?: return false
return resolvedCall.resultingDescriptor.fqNameUnsafe.asString() == fqMethodName
}
private fun KtLambdaExpression.getParameterName(): String? {
val parameters = valueParameters
if (parameters.size > 1) return null
return if (parameters.size == 1) parameters[0].text else "it"
}
private fun KtDotQualifiedExpression.getLeftMostReceiverExpression(): KtExpression =
(receiverExpression as? KtDotQualifiedExpression)?.getLeftMostReceiverExpression() ?: receiverExpression
private fun KtDotQualifiedExpression.receiverUsedAsArgument(receiverName: String): Boolean {
if ((selectorExpression as? KtCallExpression)?.valueArguments?.firstOrNull { it.text == receiverName } != null) return true
return (receiverExpression as? KtDotQualifiedExpression)?.receiverUsedAsArgument(receiverName) ?: false
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.ReplaceSingleLineLetIntention
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.let {
it.length<caret>
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.length
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
it.length<caret>
it.length
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String = ""
foo.let {
it.length<caret>
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String = ""
foo.length
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
text ->
"".to("")<caret>
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
"".to("")<caret>
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
it.to(it)<caret>
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
it.to(it).to("").to("")<caret>
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
it.to("").to(it).to("")<caret>
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
it.to("").to("").to(it)<caret>
}
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
val foo: String? = null
foo?.let {
text ->
text.to(text)<caret>
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.let {
it.to("")<caret>
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.to("")
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.let {
it.to("").to("")<caret>
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.to("").to("")
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.let {
text ->
text.length<caret>
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.length
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.toString()?.let {
it.to("")<caret>
}?.let {
it.to("")
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun foo() {
val foo: String? = null
foo?.toString()?.to("")?.let {
it.to("")
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun Int.foo(): Int {
return <caret>let { it.hashCode().hashCode() }
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun Int.foo(): Int {
return hashCode().hashCode()
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun Int.foo(): Int {
return <caret>let { it.hashCode() }
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
// IS_APPLICABLE: true
fun Int.foo(): Int {
return hashCode()
}
@@ -10565,6 +10565,111 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/replaceSingleLineLetIntention")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceSingleLineLetIntention extends AbstractIntentionTest {
public void testAllFilesPresentInReplaceSingleLineLetIntention() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/replaceSingleLineLetIntention"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("let.kt")
public void testLet() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/let.kt");
doTest(fileName);
}
@TestMetadata("letMultipleLines.kt")
public void testLetMultipleLines() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letMultipleLines.kt");
doTest(fileName);
}
@TestMetadata("letNoSafeCall.kt")
public void testLetNoSafeCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letNoSafeCall.kt");
doTest(fileName);
}
@TestMetadata("letNotUseParameterReceiver.kt")
public void testLetNotUseParameterReceiver() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letNotUseParameterReceiver.kt");
doTest(fileName);
}
@TestMetadata("letNotUseReceiver.kt")
public void testLetNotUseReceiver() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letNotUseReceiver.kt");
doTest(fileName);
}
@TestMetadata("letUseIt.kt")
public void testLetUseIt() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letUseIt.kt");
doTest(fileName);
}
@TestMetadata("letUseItWithMultipleMethodCall1.kt")
public void testLetUseItWithMultipleMethodCall1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letUseItWithMultipleMethodCall1.kt");
doTest(fileName);
}
@TestMetadata("letUseItWithMultipleMethodCall2.kt")
public void testLetUseItWithMultipleMethodCall2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letUseItWithMultipleMethodCall2.kt");
doTest(fileName);
}
@TestMetadata("letUseItWithMultipleMethodCall3.kt")
public void testLetUseItWithMultipleMethodCall3() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letUseItWithMultipleMethodCall3.kt");
doTest(fileName);
}
@TestMetadata("letUseParameter.kt")
public void testLetUseParameter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letUseParameter.kt");
doTest(fileName);
}
@TestMetadata("letWithMethodCall.kt")
public void testLetWithMethodCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letWithMethodCall.kt");
doTest(fileName);
}
@TestMetadata("letWithMultipleMethodCall.kt")
public void testLetWithMultipleMethodCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letWithMultipleMethodCall.kt");
doTest(fileName);
}
@TestMetadata("letWithParameter.kt")
public void testLetWithParameter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/letWithParameter.kt");
doTest(fileName);
}
@TestMetadata("sameLets.kt")
public void testSameLets() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/sameLets.kt");
doTest(fileName);
}
@TestMetadata("this.kt")
public void testThis() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/this.kt");
doTest(fileName);
}
@TestMetadata("thisShort.kt")
public void testThisShort() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceSingleLineLetIntention/thisShort.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/replaceSubstringWithDropLast")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)