diff --git a/idea/resources/inspectionDescriptions/AddVarianceModifier.html b/idea/resources/inspectionDescriptions/AddVarianceModifier.html new file mode 100644 index 00000000000..3c420bcf670 --- /dev/null +++ b/idea/resources/inspectionDescriptions/AddVarianceModifier.html @@ -0,0 +1,6 @@ + + +This inspection reports type parameters that can have 'in' or 'out' variance added. +Usage of 'in' and 'out' variances leads to more precise type inference in Kotlin. + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 39e07135858..5049ee29676 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1423,6 +1423,13 @@ level="WEAK WARNING" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/AddVarianceModifierInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/AddVarianceModifierInspection.kt new file mode 100644 index 00000000000..12c000cdc4f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/AddVarianceModifierInspection.kt @@ -0,0 +1,87 @@ +/* + * 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.* +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.diagnostics.DiagnosticSink +import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtTypeParameter +import org.jetbrains.kotlin.psi.KtVisitorVoid +import org.jetbrains.kotlin.psi.addRemoveModifier.addModifier +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.ManualVariance +import org.jetbrains.kotlin.resolve.VarianceCheckerCore +import org.jetbrains.kotlin.types.Variance + +class AddVarianceModifierInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { + return object : KtVisitorVoid() { + + private fun variancePossible( + klass: KtClassOrObject, + parameterDescriptor: TypeParameterDescriptor, + variance: Variance, + context: BindingContext + ) = VarianceCheckerCore( + context, + DiagnosticSink.DO_NOTHING, + ManualVariance(parameterDescriptor, variance) + ).checkClassOrObject(klass) + + override fun visitClassOrObject(klass: KtClassOrObject) { + val context = klass.analyzeFully() + for (typeParameter in klass.typeParameters) { + if (typeParameter.variance != Variance.INVARIANT) continue + val parameterDescriptor = + context.get(BindingContext.DECLARATION_TO_DESCRIPTOR, typeParameter) as? TypeParameterDescriptor ?: continue + val variances = listOf(Variance.IN_VARIANCE, Variance.OUT_VARIANCE).filter { + variancePossible(klass, parameterDescriptor, it, context) + } + if (variances.isNotEmpty()) { + val suggested = variances.joinToString(" or ") { "'$it'" } + val fixes = variances.map { AddVarianceFix(it) } + holder.registerProblem( + typeParameter, + "Type parameter can have $suggested variance", + ProblemHighlightType.WEAK_WARNING, + *fixes.toTypedArray() + ) + } + } + } + } + } + + class AddVarianceFix(val variance: Variance) : LocalQuickFix { + override fun getName() = "Add '$variance' variance" + + override fun getFamilyName() = "Add variance" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val typeParameter = descriptor.psiElement as? KtTypeParameter + ?: throw AssertionError("Add variance fix is used on ${descriptor.psiElement.text}") + addModifier(typeParameter, if (variance == Variance.IN_VARIANCE) KtTokens.IN_KEYWORD else KtTokens.OUT_KEYWORD) + } + + } + +} \ No newline at end of file diff --git a/idea/testData/inspections/addVarianceModifier/inspectionData/expected.xml b/idea/testData/inspections/addVarianceModifier/inspectionData/expected.xml new file mode 100644 index 00000000000..07cddc55bc5 --- /dev/null +++ b/idea/testData/inspections/addVarianceModifier/inspectionData/expected.xml @@ -0,0 +1,42 @@ + + + test.kt + 1 + light_idea_test_case + + Type parameter can have 'in' or 'out' variance + Type parameter can have 'out' variance + + + test.kt + 5 + light_idea_test_case + + Type parameter can have 'in' or 'out' variance + Type parameter can have 'in' variance + + + test.kt + 17 + light_idea_test_case + + Type parameter can have 'in' or 'out' variance + Type parameter can have 'out' variance + + + test.kt + 21 + light_idea_test_case + + Type parameter can have 'in' or 'out' variance + Type parameter can have 'in' variance + + + test.kt + 24 + light_idea_test_case + + Type parameter can have 'in' or 'out' variance + Type parameter can have 'in' or 'out' variance + + \ No newline at end of file diff --git a/idea/testData/inspections/addVarianceModifier/inspectionData/inspections.test b/idea/testData/inspections/addVarianceModifier/inspectionData/inspections.test new file mode 100644 index 00000000000..6fe0bce31c7 --- /dev/null +++ b/idea/testData/inspections/addVarianceModifier/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.AddVarianceModifierInspection \ No newline at end of file diff --git a/idea/testData/inspections/addVarianceModifier/test.kt b/idea/testData/inspections/addVarianceModifier/test.kt new file mode 100644 index 00000000000..7c34367a60b --- /dev/null +++ b/idea/testData/inspections/addVarianceModifier/test.kt @@ -0,0 +1,24 @@ +interface EffectivelyOut { + fun foo(): T + val bar: T +} +interface EffectivelyIn { + fun foo(arg: T) +} +interface Invariant1 { + var bar: T +} +interface Invariant2 { + fun T.foo(): T +} +interface Invariant3> { + fun T.foo() +} +abstract class AbstractOut { + abstract val foo: T + private var bar = foo +} +abstract class AbstractIn(private val foo: T) { + fun bar(arg: T) = foo == arg +} +interface Empty \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/.inspection b/idea/testData/quickfix/addVarianceModifier/.inspection new file mode 100644 index 00000000000..34b24d475b9 --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.AddVarianceModifierInspection \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/Empty.kt b/idea/testData/quickfix/addVarianceModifier/Empty.kt new file mode 100644 index 00000000000..0987b32a730 --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/Empty.kt @@ -0,0 +1,2 @@ +// "Add 'out' variance" "true" +interface Empty<T> \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/abstractIn.kt b/idea/testData/quickfix/addVarianceModifier/abstractIn.kt new file mode 100644 index 00000000000..e4d20856030 --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/abstractIn.kt @@ -0,0 +1,4 @@ +// "Add 'in' variance" "true" +abstract class AbstractIn<T>(private val foo: T) { + fun bar(arg: T) = foo == arg +} \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/abstractIn.kt.after b/idea/testData/quickfix/addVarianceModifier/abstractIn.kt.after new file mode 100644 index 00000000000..8b2ce6e9751 --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/abstractIn.kt.after @@ -0,0 +1,4 @@ +// "Add 'in' variance" "true" +abstract class AbstractIn(private val foo: T) { + fun bar(arg: T) = foo == arg +} \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/abstractOut.kt b/idea/testData/quickfix/addVarianceModifier/abstractOut.kt new file mode 100644 index 00000000000..3676829921a --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/abstractOut.kt @@ -0,0 +1,5 @@ +// "Add 'out' variance" "true" +abstract class AbstractOut<T> { + abstract val foo: T + private var bar = foo +} \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/abstractOut.kt.after b/idea/testData/quickfix/addVarianceModifier/abstractOut.kt.after new file mode 100644 index 00000000000..6352b61c8e7 --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/abstractOut.kt.after @@ -0,0 +1,5 @@ +// "Add 'out' variance" "true" +abstract class AbstractOut { + abstract val foo: T + private var bar = foo +} \ No newline at end of file diff --git a/idea/testData/quickfix/addVarianceModifier/empty.kt.after b/idea/testData/quickfix/addVarianceModifier/empty.kt.after new file mode 100644 index 00000000000..face154ad24 --- /dev/null +++ b/idea/testData/quickfix/addVarianceModifier/empty.kt.after @@ -0,0 +1,2 @@ +// "Add 'out' variance" "true" +interface Empty \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 51049ace3df..7bb03f0a16d 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -102,6 +102,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest { @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) public static class Inspections extends AbstractInspectionTest { + @TestMetadata("addVarianceModifier/inspectionData/inspections.test") + public void testAddVarianceModifier_inspectionData_Inspections_test() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/addVarianceModifier/inspectionData/inspections.test"); + doTest(fileName); + } + public void testAllFilesPresentInInspections() throws Exception { KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/inspections"), Pattern.compile("^(inspections\\.test)$")); } diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index b4384fed0a7..cb32252fc9a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -509,6 +509,33 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/addVarianceModifier") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class AddVarianceModifier extends AbstractQuickFixTest { + @TestMetadata("abstractIn.kt") + public void testAbstractIn() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addVarianceModifier/abstractIn.kt"); + doTest(fileName); + } + + @TestMetadata("abstractOut.kt") + public void testAbstractOut() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addVarianceModifier/abstractOut.kt"); + doTest(fileName); + } + + public void testAllFilesPresentInAddVarianceModifier() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addVarianceModifier"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); + } + + @TestMetadata("empty.kt") + public void testEmpty() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addVarianceModifier/empty.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/quickfix/asyncUnsupported") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) @@ -7368,8 +7395,7 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { @TestMetadata("letClassImplementInterfaceNullable.kt") public void testLetClassImplementInterfaceNullable() throws Exception { - String fileName = - KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/letClassImplementInterfaceNullable.kt"); + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/typeMismatch/letClassImplementInterfaceNullable.kt"); doTest(fileName); }