diff --git a/idea/resources/inspectionDescriptions/ArrayInDataClass.html b/idea/resources/inspectionDescriptions/ArrayInDataClass.html new file mode 100644 index 00000000000..e9f0e0f84b9 --- /dev/null +++ b/idea/resources/inspectionDescriptions/ArrayInDataClass.html @@ -0,0 +1,6 @@ + + +This inspection reports array properties in a data class without overridden equals() or hashCode() inside. +Due to default equals() behaviour for arrays in JVM, it's strongly recommended to override equals() and hashCode() in such cases. + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 07ad96ea547..5847ea3ef12 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1418,6 +1418,13 @@ level="WARNING" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/ArrayInDataClassInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/ArrayInDataClassInspection.kt new file mode 100644 index 00000000000..d5cbe41afb2 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/ArrayInDataClassInspection.kt @@ -0,0 +1,74 @@ +/* + * 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.inspections + +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.KtVisitorVoid +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.util.OperatorNameConventions + +class ArrayInDataClassInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { + return object : KtVisitorVoid() { + override fun visitClass(klass: KtClass) { + if (!klass.isData()) return + val constructor = klass.getPrimaryConstructor() ?: return + if (hasOverriddenEqualsAndHashCode(klass)) return + val context = constructor.analyze(BodyResolveMode.PARTIAL) + for (parameter in constructor.valueParameters) { + if (!parameter.hasValOrVar()) continue + val type = context.get(BindingContext.TYPE, parameter.typeReference) ?: continue + if (KotlinBuiltIns.isArray(type) || KotlinBuiltIns.isPrimitiveArray(type)) { + holder.registerProblem(parameter, + "Array property in data class: it's recommended to override equals() / hashCode()", + ProblemHighlightType.WEAK_WARNING) + } + } + } + + private fun hasOverriddenEqualsAndHashCode(klass: KtClass): Boolean { + var overriddenEquals = false + var overriddenHashCode = false + for (declaration in klass.declarations) { + if (declaration !is KtFunction) continue + if (!declaration.hasModifier(KtTokens.OVERRIDE_KEYWORD)) continue + if (declaration.nameAsName == OperatorNameConventions.EQUALS && declaration.valueParameters.size == 1) { + val parameter = declaration.valueParameters.single() + val context = declaration.analyze(BodyResolveMode.PARTIAL) + val type = context.get(BindingContext.TYPE, parameter.typeReference) + if (type != null && KotlinBuiltIns.isNullableAny(type)) { + overriddenEquals = true + } + } + if (declaration.name == "hashCode" && declaration.valueParameters.size == 0) { + overriddenHashCode = true + } + } + return overriddenEquals && overriddenHashCode + } + } + } +} diff --git a/idea/testData/inspections/arrayInDataClass/inspectionData/expected.xml b/idea/testData/inspections/arrayInDataClass/inspectionData/expected.xml new file mode 100644 index 00000000000..def5ff38916 --- /dev/null +++ b/idea/testData/inspections/arrayInDataClass/inspectionData/expected.xml @@ -0,0 +1,18 @@ + + + test.kt + 1 + light_idea_test_case + + Array property in data class + Array property in data class: it's recommended to override equals() / hashCode() + + + test.kt + 5 + light_idea_test_case + + Array property in data class + Array property in data class: it's recommended to override equals() / hashCode() + + diff --git a/idea/testData/inspections/arrayInDataClass/inspectionData/inspections.test b/idea/testData/inspections/arrayInDataClass/inspectionData/inspections.test new file mode 100644 index 00000000000..7ec96680af7 --- /dev/null +++ b/idea/testData/inspections/arrayInDataClass/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.ArrayInDataClassInspection \ No newline at end of file diff --git a/idea/testData/inspections/arrayInDataClass/test.kt b/idea/testData/inspections/arrayInDataClass/test.kt new file mode 100644 index 00000000000..92f69706d2b --- /dev/null +++ b/idea/testData/inspections/arrayInDataClass/test.kt @@ -0,0 +1,11 @@ +data class First(val x: Array, val y: Int) + +class Second(val x: Array) + +data class Third(val x: String, val y: IntArray) + +data class Correct(val x: IntArray) { + override fun equals(other: Any?) = other is Correct + + override fun hashCode() = 0 +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 1eb091d8af2..260a0448935 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -112,6 +112,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest { doTest(fileName); } + @TestMetadata("arrayInDataClass/inspectionData/inspections.test") + public void testArrayInDataClass_inspectionData_Inspections_test() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/arrayInDataClass/inspectionData/inspections.test"); + doTest(fileName); + } + @TestMetadata("canBeVal/inspectionData/inspections.test") public void testCanBeVal_inspectionData_Inspections_test() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/canBeVal/inspectionData/inspections.test");