From de3fbe38f16dcc1ed27e9917b032ff988eee3197 Mon Sep 17 00:00:00 2001 From: Mikhail Glukhikh Date: Mon, 30 May 2016 18:19:26 +0300 Subject: [PATCH] KT-12152: quick fix "make final" for member / containing class --- .../kotlin/idea/inspections/AddModifierFix.kt | 37 +++++++++++ .../idea/inspections/LeakingThisInspection.kt | 63 ++++++++++++++++--- .../testData/quickfix/leakingThis/.inspection | 1 + .../leakingThis/accessOpenProperty.kt | 5 ++ .../leakingThis/accessOpenProperty.kt.after | 5 ++ .../leakingThis/accessOpenPropertyClass.kt | 5 ++ .../accessOpenPropertyClass.kt.after | 5 ++ .../leakingThis/accessOverriddenProperty.kt | 10 +++ .../quickfix/leakingThis/callOpenMethod.kt | 10 +++ .../leakingThis/callOpenMethod.kt.after | 10 +++ .../leakingThis/callOpenMethodClass.kt | 10 +++ .../leakingThis/callOpenMethodClass.kt.after | 10 +++ .../quickfix/leakingThis/inAbstract.kt | 10 +++ .../quickfix/leakingThis/inNonFinal.kt | 9 +++ .../quickfix/leakingThis/inNonFinal.kt.after | 9 +++ .../quickfix/leakingThis/inOverridden.kt | 12 ++++ .../idea/quickfix/QuickFixTestGenerated.java | 57 +++++++++++++++++ 17 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/AddModifierFix.kt create mode 100644 idea/testData/quickfix/leakingThis/.inspection create mode 100644 idea/testData/quickfix/leakingThis/accessOpenProperty.kt create mode 100644 idea/testData/quickfix/leakingThis/accessOpenProperty.kt.after create mode 100644 idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt create mode 100644 idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt.after create mode 100644 idea/testData/quickfix/leakingThis/accessOverriddenProperty.kt create mode 100644 idea/testData/quickfix/leakingThis/callOpenMethod.kt create mode 100644 idea/testData/quickfix/leakingThis/callOpenMethod.kt.after create mode 100644 idea/testData/quickfix/leakingThis/callOpenMethodClass.kt create mode 100644 idea/testData/quickfix/leakingThis/callOpenMethodClass.kt.after create mode 100644 idea/testData/quickfix/leakingThis/inAbstract.kt create mode 100644 idea/testData/quickfix/leakingThis/inNonFinal.kt create mode 100644 idea/testData/quickfix/leakingThis/inNonFinal.kt.after create mode 100644 idea/testData/quickfix/leakingThis/inOverridden.kt diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/AddModifierFix.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/AddModifierFix.kt new file mode 100644 index 00000000000..3224d76fdd0 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/AddModifierFix.kt @@ -0,0 +1,37 @@ +/* + * 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.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.lexer.KtModifierKeywordToken +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.addRemoveModifier.addModifier + +open class AddModifierFix(val modifierListOwner: KtModifierListOwner, + val modifier: KtModifierKeywordToken, + val text: String) : LocalQuickFix { + + override fun getName() = text + + override fun getFamilyName() = text + + final override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + addModifier(modifierListOwner, modifier) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/LeakingThisInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/LeakingThisInspection.kt index 5e2c08d9fc2..06ad8f88557 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/LeakingThisInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/LeakingThisInspection.kt @@ -17,13 +17,24 @@ package org.jetbrains.kotlin.idea.inspections import com.intellij.codeInspection.LocalInspectionToolSession +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 com.intellij.psi.PsiElementVisitor +import com.intellij.psi.search.searches.DefinitionsScopedSearch import org.jetbrains.kotlin.cfg.LeakingThisDescriptor.* +import org.jetbrains.kotlin.descriptors.MemberDescriptor +import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully +import org.jetbrains.kotlin.lexer.KtModifierKeywordToken +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.BindingContext.LEAKING_THIS +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.psi.addRemoveModifier.addModifier class LeakingThisInspection : AbstractKotlinInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { @@ -32,26 +43,58 @@ class LeakingThisInspection : AbstractKotlinInspection() { val context = expression.analyzeFully() val leakingThisDescriptor = context.get(LEAKING_THIS, expression) ?: return val description = when (leakingThisDescriptor) { - is PropertyIsNull -> null // Not supported yet + is PropertyIsNull -> return // Not supported yet is NonFinalClass -> if (expression is KtThisExpression) "Leaking 'this' in constructor of non-final class ${leakingThisDescriptor.klass.name}" else - null // Not supported yet + return // Not supported yet is NonFinalProperty -> "Accessing non-final property ${leakingThisDescriptor.property.name} in constructor" is NonFinalFunction -> "Calling non-final function ${leakingThisDescriptor.function.name} in constructor" } - if (description != null) { - holder.registerProblem( - expression, description, - when (leakingThisDescriptor) { - is NonFinalProperty, is NonFinalFunction -> GENERIC_ERROR_OR_WARNING - else -> WEAK_WARNING - } - ) + val memberDescriptorToFix = when (leakingThisDescriptor) { + is NonFinalProperty -> leakingThisDescriptor.property + is NonFinalFunction -> leakingThisDescriptor.function + else -> null } + val memberFix = memberDescriptorToFix?.let { + if (it.modality == Modality.OPEN) { + val modifierListOwner = DescriptorToSourceUtils.descriptorToDeclaration(it) as? KtDeclaration + MakeFinalFix.create(modifierListOwner, it.name) + } + else null + } + + val klass = leakingThisDescriptor.classOrObject as? KtClass + val classFix = + if (klass != null && klass.hasModifier(KtTokens.OPEN_KEYWORD)) { + MakeFinalFix.create(klass, klass.nameAsSafeName) + } + else null + + holder.registerProblem( + expression, description, + when (leakingThisDescriptor) { + is NonFinalProperty, is NonFinalFunction -> GENERIC_ERROR_OR_WARNING + else -> WEAK_WARNING + }, + *(arrayOf(memberFix, classFix).filterNotNull().toTypedArray()) + ) + } + } + } + + class MakeFinalFix private constructor(modifierListOwner: KtModifierListOwner, name: Name) : + AddModifierFix(modifierListOwner, KtTokens.FINAL_KEYWORD, "Make '$name' final") { + + companion object { + fun create(declaration: KtDeclaration?, name: Name): MakeFinalFix? { + declaration ?: return null + val useScope = declaration.useScope + if (DefinitionsScopedSearch.search(declaration, useScope).findFirst() != null) return null + return MakeFinalFix(declaration, name) } } } diff --git a/idea/testData/quickfix/leakingThis/.inspection b/idea/testData/quickfix/leakingThis/.inspection new file mode 100644 index 00000000000..51921b4fe0d --- /dev/null +++ b/idea/testData/quickfix/leakingThis/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.LeakingThisInspection \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/accessOpenProperty.kt b/idea/testData/quickfix/leakingThis/accessOpenProperty.kt new file mode 100644 index 00000000000..62f32c4093c --- /dev/null +++ b/idea/testData/quickfix/leakingThis/accessOpenProperty.kt @@ -0,0 +1,5 @@ +// "Make 'x' final" "true" + +open class My(open val x: Int) { + val y = x +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/accessOpenProperty.kt.after b/idea/testData/quickfix/leakingThis/accessOpenProperty.kt.after new file mode 100644 index 00000000000..403f30724c8 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/accessOpenProperty.kt.after @@ -0,0 +1,5 @@ +// "Make 'x' final" "true" + +open class My(val x: Int) { + val y = x +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt b/idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt new file mode 100644 index 00000000000..3e276be37a4 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt @@ -0,0 +1,5 @@ +// "Make 'My' final" "true" + +open class My(open val x: Int) { + val y = x +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt.after b/idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt.after new file mode 100644 index 00000000000..3e118d0283d --- /dev/null +++ b/idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt.after @@ -0,0 +1,5 @@ +// "Make 'My' final" "true" + +class My(open val x: Int) { + val y = x +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/accessOverriddenProperty.kt b/idea/testData/quickfix/leakingThis/accessOverriddenProperty.kt new file mode 100644 index 00000000000..2ff06f05af1 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/accessOverriddenProperty.kt @@ -0,0 +1,10 @@ +// "Make 'x' final" "false" +// ACTION: Convert property initializer to getter + +open class My(open val x: Int) { + val y = x +} + +class Your(x : Int) : My(x) { + override val x: Int get() = 42 +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/callOpenMethod.kt b/idea/testData/quickfix/leakingThis/callOpenMethod.kt new file mode 100644 index 00000000000..924156ce9b3 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/callOpenMethod.kt @@ -0,0 +1,10 @@ +// "Make 'init' final" "true" + +open class My { + + init { + init() + } + + open fun init() {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/callOpenMethod.kt.after b/idea/testData/quickfix/leakingThis/callOpenMethod.kt.after new file mode 100644 index 00000000000..6e4acd96a77 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/callOpenMethod.kt.after @@ -0,0 +1,10 @@ +// "Make 'init' final" "true" + +open class My { + + init { + init() + } + + fun init() {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/callOpenMethodClass.kt b/idea/testData/quickfix/leakingThis/callOpenMethodClass.kt new file mode 100644 index 00000000000..c304bf6b5c1 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/callOpenMethodClass.kt @@ -0,0 +1,10 @@ +// "Make 'My' final" "true" + +open class My { + + init { + init() + } + + open fun init() {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/callOpenMethodClass.kt.after b/idea/testData/quickfix/leakingThis/callOpenMethodClass.kt.after new file mode 100644 index 00000000000..d874a6195e7 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/callOpenMethodClass.kt.after @@ -0,0 +1,10 @@ +// "Make 'My' final" "true" + +class My { + + init { + init() + } + + open fun init() {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/inAbstract.kt b/idea/testData/quickfix/leakingThis/inAbstract.kt new file mode 100644 index 00000000000..4be3c129415 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/inAbstract.kt @@ -0,0 +1,10 @@ +// "Make 'My' final" "false" +// ACTION: Add 'my =' to argument + +abstract class My { + init { + register(this) + } +} + +fun register(my: My) {} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/inNonFinal.kt b/idea/testData/quickfix/leakingThis/inNonFinal.kt new file mode 100644 index 00000000000..b1858014365 --- /dev/null +++ b/idea/testData/quickfix/leakingThis/inNonFinal.kt @@ -0,0 +1,9 @@ +// "Make 'My' final" "true" + +open class My { + init { + register(this) + } +} + +fun register(my: My) {} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/inNonFinal.kt.after b/idea/testData/quickfix/leakingThis/inNonFinal.kt.after new file mode 100644 index 00000000000..9dd50a0550d --- /dev/null +++ b/idea/testData/quickfix/leakingThis/inNonFinal.kt.after @@ -0,0 +1,9 @@ +// "Make 'My' final" "true" + +class My { + init { + register(this) + } +} + +fun register(my: My) {} \ No newline at end of file diff --git a/idea/testData/quickfix/leakingThis/inOverridden.kt b/idea/testData/quickfix/leakingThis/inOverridden.kt new file mode 100644 index 00000000000..e3cb871707f --- /dev/null +++ b/idea/testData/quickfix/leakingThis/inOverridden.kt @@ -0,0 +1,12 @@ +// "Make 'My' final" "false" +// ACTION: Add 'my =' to argument + +open class My { + init { + register(this) + } +} + +class Derived : My() + +fun register(my: My) {} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index cd56ff226c0..050805c3b87 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -5101,6 +5101,63 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/leakingThis") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class LeakingThis extends AbstractQuickFixTest { + @TestMetadata("accessOpenProperty.kt") + public void testAccessOpenProperty() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/accessOpenProperty.kt"); + doTest(fileName); + } + + @TestMetadata("accessOpenPropertyClass.kt") + public void testAccessOpenPropertyClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/accessOpenPropertyClass.kt"); + doTest(fileName); + } + + @TestMetadata("accessOverriddenProperty.kt") + public void testAccessOverriddenProperty() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/accessOverriddenProperty.kt"); + doTest(fileName); + } + + public void testAllFilesPresentInLeakingThis() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/leakingThis"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); + } + + @TestMetadata("callOpenMethod.kt") + public void testCallOpenMethod() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/callOpenMethod.kt"); + doTest(fileName); + } + + @TestMetadata("callOpenMethodClass.kt") + public void testCallOpenMethodClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/callOpenMethodClass.kt"); + doTest(fileName); + } + + @TestMetadata("inAbstract.kt") + public void testInAbstract() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/inAbstract.kt"); + doTest(fileName); + } + + @TestMetadata("inNonFinal.kt") + public void testInNonFinal() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/inNonFinal.kt"); + doTest(fileName); + } + + @TestMetadata("inOverridden.kt") + public void testInOverridden() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/leakingThis/inOverridden.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/quickfix/libraries") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)