Add inspection + intention to replace .let { it.foo() } with .foo() #KT-13551 Fixed
This commit is contained in:
committed by
Mikhail Glukhikh
parent
cdf4309e04
commit
e4c873dc6a
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
+10
@@ -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>
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// WITH_RUNTIME
|
||||
// IS_APPLICABLE: false
|
||||
|
||||
fun foo() {
|
||||
val foo: String? = null
|
||||
foo?.let {
|
||||
it.to(it).to("").to("")<caret>
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// WITH_RUNTIME
|
||||
// IS_APPLICABLE: false
|
||||
|
||||
fun foo() {
|
||||
val foo: String? = null
|
||||
foo?.let {
|
||||
it.to("").to(it).to("")<caret>
|
||||
}
|
||||
}
|
||||
+9
@@ -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>
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// WITH_RUNTIME
|
||||
// IS_APPLICABLE: true
|
||||
|
||||
fun foo() {
|
||||
val foo: String? = null
|
||||
foo?.to("")
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// WITH_RUNTIME
|
||||
// IS_APPLICABLE: true
|
||||
|
||||
fun foo() {
|
||||
val foo: String? = null
|
||||
foo?.let {
|
||||
it.to("").to("")<caret>
|
||||
}
|
||||
}
|
||||
+7
@@ -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>
|
||||
}
|
||||
}
|
||||
+7
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user