Add inspections for implicit and explicit 'this' #KT-4580 Fixed

This commit is contained in:
Kirill
2017-10-20 20:54:01 +03:00
committed by Mikhail Glukhikh
parent 817f79520a
commit 89c4196c06
41 changed files with 694 additions and 1 deletions
@@ -26,15 +26,28 @@ import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
import org.jetbrains.kotlin.resolve.scopes.utils.getImplicitReceiversHierarchy
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
import java.util.*
fun LexicalScope.getImplicitReceiversWithInstance(): Collection<ReceiverParameterDescriptor>
= getImplicitReceiversWithInstanceToExpression().keys
interface ReceiverExpressionFactory {
val isImmediate: Boolean
val expressionText: String
fun createExpression(psiFactory: KtPsiFactory, shortThis: Boolean = true): KtExpression
}
fun LexicalScope.getFactoryForImplicitReceiverWithSubtypeOf(receiverType: KotlinType): ReceiverExpressionFactory? {
return getImplicitReceiversWithInstanceToExpression()
.entries
.firstOrNull { (receiverDescriptor, _) ->
receiverDescriptor.type.isSubtypeOf(receiverType)
}
?.value
}
fun LexicalScope.getImplicitReceiversWithInstanceToExpression(): Map<ReceiverParameterDescriptor, ReceiverExpressionFactory?> {
// we use a set to workaround a bug with receiver for companion object present twice in the result of getImplicitReceiversHierarchy()
val receivers = LinkedHashSet(getImplicitReceiversHierarchy())
@@ -43,7 +56,7 @@ fun LexicalScope.getImplicitReceiversWithInstanceToExpression(): Map<ReceiverPar
var current: DeclarationDescriptor? = ownerDescriptor
while (current != null) {
if (current is PropertyAccessorDescriptor) {
current = current.correspondingProperty
current = current.correspondingProperty
}
outerDeclarationsWithInstance.add(current)
@@ -75,6 +88,8 @@ fun LexicalScope.getImplicitReceiversWithInstanceToExpression(): Map<ReceiverPar
}
val factory = if (expressionText != null)
object : ReceiverExpressionFactory {
override val isImmediate = isImmediateThis
override val expressionText get() = expressionText!!
override fun createExpression(psiFactory: KtPsiFactory, shortThis: Boolean): KtExpression {
return psiFactory.createExpression(if (shortThis && isImmediateThis) "this" else expressionText)
}
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports usages of explicit 'this' when it can be omitted.
</body>
</html>
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports usages of implicit 'this'.
</body>
</html>
+19
View File
@@ -2623,6 +2623,25 @@
level="WARNING"
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ExplicitThisInspection"
displayName="Redundant explicit 'this'"
groupPath="Kotlin"
groupName="Redundant constructs"
enabledByDefault="true"
level="INFORMATION"
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ImplicitThisInspection"
displayName="Implicit 'this'"
groupPath="Kotlin"
groupName="Style issues"
enabledByDefault="true"
level="INFORMATION"
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.RedundantUnitExpressionInspection"
displayName="Redundant 'Unit'"
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2017 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.inspections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType.LIKE_UNUSED_SYMBOL
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
import org.jetbrains.kotlin.idea.util.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
class ExplicitThisInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : KtVisitorVoid() {
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
val thisExpression = expression.receiverExpression as? KtThisExpression ?: return
val context = expression.analyzeFully()
val scope = expression.getResolutionScope(context) ?: return
val selectorExpression = expression.selectorExpression as? KtReferenceExpression ?: return
val referenceExpression = selectorExpression as? KtNameReferenceExpression
?: selectorExpression.getChildOfType()
?: return
val scopeFunction = when (selectorExpression) {
is KtNameReferenceExpression -> LexicalScope::getAllAccessibleVariables
is KtCallExpression -> LexicalScope::getAllAccessibleFunctions
else -> return
}
//we avoid overload-related problems by enforcing that there is only one candidate
val candidates = scopeFunction(scope, referenceExpression.getReferencedNameAsName())
if (candidates.size != 1) return
val receiverType = context[BindingContext.EXPRESSION_TYPE_INFO, thisExpression]?.type ?: return
val expressionFactory = scope.getFactoryForImplicitReceiverWithSubtypeOf(receiverType) ?: return
val label = thisExpression.getLabelName() ?: ""
if (!expressionFactory.matchesLabel(label)) return
holder.registerProblem(thisExpression, "Redundant explicit this", LIKE_UNUSED_SYMBOL, Fix(thisExpression.text))
}
private fun ReceiverExpressionFactory.matchesLabel(label: String): Boolean {
val implicitLabel = expressionText.substringAfter("@", "")
return label == implicitLabel || (label == "" && isImmediate)
}
}
private class Fix(private val text: String) : LocalQuickFix {
override fun getFamilyName(): String = "Remove redundant '$text'"
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val thisExpression = descriptor.psiElement as? KtThisExpression ?: return
val parent = thisExpression.parent as? KtDotQualifiedExpression ?: return
parent.replace(parent.selectorExpression ?: return)
}
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2010-2017 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.inspections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
import org.jetbrains.kotlin.idea.util.getFactoryForImplicitReceiverWithSubtypeOf
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.resolve.BindingContext
class ImplicitThisInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : KtVisitorVoid() {
override fun visitExpression(expression: KtExpression) {
if (expression !is KtNameReferenceExpression) return
if (expression.isSelectorOfDotQualifiedExpression()) return
val parent = expression.parent
if (parent is KtCallExpression && parent.isSelectorOfDotQualifiedExpression()) return
val referenceExpression = expression as? KtNameReferenceExpression
?: expression.getChildOfType()
?: return
val context = expression.analyzeFully()
val scope = expression.getResolutionScope(context) ?: return
val descriptor = context[BindingContext.REFERENCE_TARGET, referenceExpression] as? CallableDescriptor ?: return
val receiverDescriptor = descriptor.extensionReceiverParameter ?: descriptor.dispatchReceiverParameter ?: return
val receiverType = receiverDescriptor.type
val expressionFactory = scope.getFactoryForImplicitReceiverWithSubtypeOf(receiverType) ?: return
val receiverText = if (expressionFactory.isImmediate) "this" else expressionFactory.expressionText
holder.registerProblem(expression, "Add explicit '$receiverText'", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, Fix(receiverText))
}
private fun KtExpression.isSelectorOfDotQualifiedExpression(): Boolean {
val parent = parent
return parent is KtDotQualifiedExpression && parent.selectorExpression == this
}
}
private class Fix(private val receiverText: String) : LocalQuickFix {
override fun getFamilyName() = "Add explicit '$receiverText'"
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val expression = descriptor.psiElement as? KtExpression ?: return
val factory = KtPsiFactory(project)
val call = expression.parent as? KtCallExpression ?: expression
call.replace(factory.createExpressionByPattern("$0.$1", receiverText, call.text))
}
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ExplicitThisInspection
@@ -0,0 +1,12 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
val s = ""
fun test() {
Foo().apply {
<caret>this@Foo.s
}
}
}
@@ -0,0 +1,12 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
fun test() {
Foo().apply {
<caret>this@Foo.s()
}
}
}
fun Foo.s() {}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
val s = ""
fun test() {
Bar().apply {
<caret>this@Foo.s
}
}
}
class Bar {
val s = ""
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
fun test() {
Bar().apply {
<caret>this@Foo.s()
}
}
}
class Bar
fun Foo.s() {}
fun Bar.s() {}
@@ -0,0 +1,7 @@
class Foo {
fun test() {
<caret>this.s()
}
}
fun Foo.s() = ""
@@ -0,0 +1,7 @@
class Foo {
fun test() {
<caret>s()
}
}
fun Foo.s() = ""
@@ -0,0 +1,7 @@
class Foo {
fun s() = ""
fun test() {
<caret>this.s()
}
}
@@ -0,0 +1,7 @@
class Foo {
fun s() = ""
fun test() {
<caret>s()
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
class Foo {
val s = ""
fun test() {
"".apply {
<caret>this@Foo.s
}
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
class Foo {
val s = ""
fun test() {
"".apply {
<caret>s
}
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
class Foo {
fun test() {
"".apply {
<caret>this@Foo.s()
}
}
}
fun Foo.s() = ""
@@ -0,0 +1,11 @@
// WITH_RUNTIME
class Foo {
fun test() {
"".apply {
<caret>s()
}
}
}
fun Foo.s() = ""
@@ -0,0 +1,18 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
val s = ""
fun test() {
Bar().apply {
"".run {
<caret>this@apply.s
}
}
}
}
class Bar {
val s = ""
}
@@ -0,0 +1,17 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
fun s(a: String) {}
fun test() {
Bar().apply {
<caret>this@Foo.s("")
}
}
}
class Bar {
fun s() {}
}
@@ -0,0 +1,17 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
fun test() {
Bar().apply {
"".run {
<caret>this@apply.s()
}
}
}
}
class Bar
fun Foo.s() {}
fun Bar.s() {}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
fun test() {
Bar().apply {
<caret>this@Foo.s("")
}
}
}
class Bar
fun Foo.s(a: String) {}
fun Bar.s() {}
@@ -0,0 +1,7 @@
class Foo {
val s = ""
fun test() {
<caret>this.s
}
}
@@ -0,0 +1,7 @@
class Foo {
val s = ""
fun test() {
<caret>s
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// PROBLEM: none
class Foo {
var s = ""
fun test() {
val s = ""
<caret>this.s = s
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ImplicitThisInspection
@@ -0,0 +1,7 @@
class Foo {
fun s() = ""
fun test() {
<caret>s()
}
}
@@ -0,0 +1,7 @@
class Foo {
fun s() = ""
fun test() {
<caret>this.s()
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
class Foo {
fun s() = ""
fun test() {
<caret>this.s()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class Foo {
fun Bar.s() = ""
}
class Bar
fun test() {
Bar().apply {
Foo().apply apply2@ {
<caret>s()
}
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class Foo {
fun Bar.s() = ""
}
class Bar
fun test() {
Bar().apply {
Foo().apply apply2@ {
<caret>this@apply.s()
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// WITH_RUNTIME
class Foo {
fun s() = ""
fun test() {
"".apply {
<caret>s()
}
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
class Foo {
fun s() = ""
fun test() {
"".apply {
<caret>this@Foo.s()
}
}
}
@@ -0,0 +1,8 @@
class Foo {
val f = Foo()
val s = ""
fun test() {
<caret>f.s
}
}
@@ -0,0 +1,8 @@
class Foo {
val f = Foo()
val s = ""
fun test() {
<caret>this.f.s
}
}
@@ -0,0 +1,7 @@
class Foo {
val s = ""
fun test() {
<caret>s
}
}
@@ -0,0 +1,7 @@
class Foo {
val s = ""
fun test() {
<caret>this.s
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
class Foo {
val s = ""
fun test() {
<caret>this.s
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
class Foo {
fun test() {
<caret>s()
}
}
fun s() = ""
@@ -621,6 +621,156 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
}
}
@TestMetadata("idea/testData/inspectionsLocal/explicitThis")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ExplicitThis extends AbstractLocalInspectionTest {
public void testAllFilesPresentInExplicitThis() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/explicitThis"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("differentReceiverInstance.kt")
public void testDifferentReceiverInstance() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/differentReceiverInstance.kt");
doTest(fileName);
}
@TestMetadata("differentReceiverInstanceExtension.kt")
public void testDifferentReceiverInstanceExtension() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/differentReceiverInstanceExtension.kt");
doTest(fileName);
}
@TestMetadata("differentReceiverType.kt")
public void testDifferentReceiverType() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/differentReceiverType.kt");
doTest(fileName);
}
@TestMetadata("differentReceiverTypeExtension.kt")
public void testDifferentReceiverTypeExtension() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/differentReceiverTypeExtension.kt");
doTest(fileName);
}
@TestMetadata("extension.kt")
public void testExtension() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/extension.kt");
doTest(fileName);
}
@TestMetadata("function.kt")
public void testFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/function.kt");
doTest(fileName);
}
@TestMetadata("multipleReceivers.kt")
public void testMultipleReceivers() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/multipleReceivers.kt");
doTest(fileName);
}
@TestMetadata("multipleReceiversExtension.kt")
public void testMultipleReceiversExtension() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/multipleReceiversExtension.kt");
doTest(fileName);
}
@TestMetadata("nestedReceivers.kt")
public void testNestedReceivers() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/nestedReceivers.kt");
doTest(fileName);
}
@TestMetadata("nestedReceiversDifferentParameters.kt")
public void testNestedReceiversDifferentParameters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/nestedReceiversDifferentParameters.kt");
doTest(fileName);
}
@TestMetadata("nestedReceiversExtension.kt")
public void testNestedReceiversExtension() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/nestedReceiversExtension.kt");
doTest(fileName);
}
@TestMetadata("nestedReceiversExtensionDifferentParameters.kt")
public void testNestedReceiversExtensionDifferentParameters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/nestedReceiversExtensionDifferentParameters.kt");
doTest(fileName);
}
@TestMetadata("property.kt")
public void testProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/property.kt");
doTest(fileName);
}
@TestMetadata("variableWithSameName.kt")
public void testVariableWithSameName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/explicitThis/variableWithSameName.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/inspectionsLocal/implicitThis")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ImplicitThis extends AbstractLocalInspectionTest {
public void testAllFilesPresentInImplicitThis() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/implicitThis"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("function.kt")
public void testFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/function.kt");
doTest(fileName);
}
@TestMetadata("functionPartOfCall.kt")
public void testFunctionPartOfCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/functionPartOfCall.kt");
doTest(fileName);
}
@TestMetadata("multipleReceivers.kt")
public void testMultipleReceivers() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/multipleReceivers.kt");
doTest(fileName);
}
@TestMetadata("nested.kt")
public void testNested() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/nested.kt");
doTest(fileName);
}
@TestMetadata("nestedCall.kt")
public void testNestedCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/nestedCall.kt");
doTest(fileName);
}
@TestMetadata("property.kt")
public void testProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/property.kt");
doTest(fileName);
}
@TestMetadata("propertyPartOfCall.kt")
public void testPropertyPartOfCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/propertyPartOfCall.kt");
doTest(fileName);
}
@TestMetadata("topLevel.kt")
public void testTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/implicitThis/topLevel.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/inspectionsLocal/kdocMissingDocumentation")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)