Add 'Covariant equals' inspection

#KT-29798 Fixed
This commit is contained in:
Toshiaki Kameyama
2019-03-08 18:25:14 +09:00
committed by Dmitry Gridin
parent 3998e842f1
commit 61f3e776a7
15 changed files with 209 additions and 0 deletions
@@ -3333,6 +3333,15 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.KotlinCovariantEqualsInspection"
displayName="Covariant 'equals()'"
groupPath="Kotlin"
groupName="Probable bugs"
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,7 @@
<html>
<body>
This inspection reports <b>equals()</b> methods taking an argument type other than <b>Any?</b>.
Only reports if the containing class does not have another <b>equals()</b> method which does take <b>Any?</b> as its argument type.
Normally, this is a mistake.
</body>
</html>
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.inspections
import com.intellij.codeInspection.ProblemsHolder
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.namedFunctionVisitor
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.typeUtil.isBoolean
import org.jetbrains.kotlin.types.typeUtil.isNullableAny
import org.jetbrains.kotlin.util.OperatorNameConventions
class KotlinCovariantEqualsInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = namedFunctionVisitor(fun(function) {
if (function.isTopLevel || function.isLocal) return
if (function.nameAsName != OperatorNameConventions.EQUALS) return
val nameIdentifier = function.nameIdentifier ?: return
val classOrObject = function.containingClassOrObject ?: return
if (classOrObject is KtObjectDeclaration && classOrObject.isCompanion()) return
val parameter = function.valueParameters.singleOrNull() ?: return
val typeReference = parameter.typeReference ?: return
val type = parameter.analyze(BodyResolveMode.PARTIAL)[BindingContext.TYPE, typeReference] ?: return
if (KotlinBuiltIns.isNullableAny(type)) return
if (classOrObject.body?.children?.any { (it as? KtNamedFunction)?.isEquals() == true } == true) return
holder.registerProblem(nameIdentifier, "'equals' should take 'Any?' as its argument")
})
private fun KtNamedFunction.isEquals(): Boolean {
if (!hasModifier(KtTokens.OVERRIDE_KEYWORD)) return false
if (nameAsName != OperatorNameConventions.EQUALS) return false
val descriptor = this.descriptor as? FunctionDescriptor ?: return false
if (descriptor.valueParameters.singleOrNull()?.type?.isNullableAny() != true) return false
if (descriptor.returnType?.isBoolean() != true) return false
return true
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.KotlinCovariantEqualsInspection
@@ -0,0 +1,7 @@
// PROBLEM: 'equals' should take 'Any?' as its argument
// FIX: none
class Foo {
fun <caret>equals(other: Foo?): Boolean {
return true
}
}
@@ -0,0 +1,10 @@
// PROBLEM: none
class Foo {
fun <caret>equals(other: Foo?): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
return true
}
}
@@ -0,0 +1,8 @@
// PROBLEM: none
class F {
companion object {
fun <caret>equals(other: F?): Boolean {
return true
}
}
}
@@ -0,0 +1,7 @@
// PROBLEM: 'equals' should take 'Any?' as its argument
// FIX: none
object F {
fun <caret>equals(other: F?): Boolean {
return true
}
}
@@ -0,0 +1,9 @@
// PROBLEM: 'equals' should take 'Any?' as its argument
// FIX: none
interface F
val f = object : F {
fun <caret>equals(other: F?): Boolean {
return true
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
class Foo {
fun test() {
fun <caret>equals(other: Foo?): Boolean {
return true
}
equals(null)
}
}
@@ -0,0 +1,13 @@
// PROBLEM: 'equals' should take 'Any?' as its argument
// FIX: none
open class Foo {
open fun equals(other: Foo?): Boolean {
return true
}
}
class Bar : Foo() {
override fun <caret>equals(other: Foo?): Boolean {
return true
}
}
@@ -0,0 +1,6 @@
// PROBLEM: none
class Foo {
override fun <caret>equals(other: Any?): Boolean {
return true
}
}
@@ -0,0 +1,4 @@
// PROBLEM: none
fun <caret>equals(other: Any?): Boolean {
return true
}
@@ -0,0 +1,6 @@
// PROBLEM: none
class Foo {
fun <caret>equals(other: Foo?, other2: Foo?): Boolean {
return true
}
}
@@ -2459,6 +2459,69 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
}
}
@TestMetadata("idea/testData/inspectionsLocal/covariantEquals")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class CovariantEquals extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInCovariantEquals() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/covariantEquals"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("basic.kt")
public void testBasic() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/basic.kt");
}
@TestMetadata("hasOverriddenAnyEquals.kt")
public void testHasOverriddenAnyEquals() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/hasOverriddenAnyEquals.kt");
}
@TestMetadata("inCompanion.kt")
public void testInCompanion() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/inCompanion.kt");
}
@TestMetadata("inObject.kt")
public void testInObject() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/inObject.kt");
}
@TestMetadata("inObjectLiteral.kt")
public void testInObjectLiteral() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/inObjectLiteral.kt");
}
@TestMetadata("local.kt")
public void testLocal() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/local.kt");
}
@TestMetadata("override.kt")
public void testOverride() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/override.kt");
}
@TestMetadata("overrideAnyEquals.kt")
public void testOverrideAnyEquals() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/overrideAnyEquals.kt");
}
@TestMetadata("topLevel.kt")
public void testTopLevel() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/topLevel.kt");
}
@TestMetadata("twoParameters.kt")
public void testTwoParameters() throws Exception {
runTest("idea/testData/inspectionsLocal/covariantEquals/twoParameters.kt");
}
}
@TestMetadata("idea/testData/inspectionsLocal/delegationToVarProperty")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)