KT-12152: quick fix "make final" for member / containing class
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,24 @@
|
|||||||
package org.jetbrains.kotlin.idea.inspections
|
package org.jetbrains.kotlin.idea.inspections
|
||||||
|
|
||||||
import com.intellij.codeInspection.LocalInspectionToolSession
|
import com.intellij.codeInspection.LocalInspectionToolSession
|
||||||
|
import com.intellij.codeInspection.LocalQuickFix
|
||||||
|
import com.intellij.codeInspection.ProblemDescriptor
|
||||||
import com.intellij.codeInspection.ProblemHighlightType.*
|
import com.intellij.codeInspection.ProblemHighlightType.*
|
||||||
import com.intellij.codeInspection.ProblemsHolder
|
import com.intellij.codeInspection.ProblemsHolder
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.psi.PsiElementVisitor
|
import com.intellij.psi.PsiElementVisitor
|
||||||
|
import com.intellij.psi.search.searches.DefinitionsScopedSearch
|
||||||
import org.jetbrains.kotlin.cfg.LeakingThisDescriptor.*
|
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.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.psi.*
|
||||||
import org.jetbrains.kotlin.resolve.BindingContext.LEAKING_THIS
|
import org.jetbrains.kotlin.resolve.BindingContext.LEAKING_THIS
|
||||||
|
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
|
||||||
|
import org.jetbrains.kotlin.psi.addRemoveModifier.addModifier
|
||||||
|
|
||||||
class LeakingThisInspection : AbstractKotlinInspection() {
|
class LeakingThisInspection : AbstractKotlinInspection() {
|
||||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||||
@@ -32,26 +43,58 @@ class LeakingThisInspection : AbstractKotlinInspection() {
|
|||||||
val context = expression.analyzeFully()
|
val context = expression.analyzeFully()
|
||||||
val leakingThisDescriptor = context.get(LEAKING_THIS, expression) ?: return
|
val leakingThisDescriptor = context.get(LEAKING_THIS, expression) ?: return
|
||||||
val description = when (leakingThisDescriptor) {
|
val description = when (leakingThisDescriptor) {
|
||||||
is PropertyIsNull -> null // Not supported yet
|
is PropertyIsNull -> return // Not supported yet
|
||||||
is NonFinalClass ->
|
is NonFinalClass ->
|
||||||
if (expression is KtThisExpression)
|
if (expression is KtThisExpression)
|
||||||
"Leaking 'this' in constructor of non-final class ${leakingThisDescriptor.klass.name}"
|
"Leaking 'this' in constructor of non-final class ${leakingThisDescriptor.klass.name}"
|
||||||
else
|
else
|
||||||
null // Not supported yet
|
return // Not supported yet
|
||||||
is NonFinalProperty ->
|
is NonFinalProperty ->
|
||||||
"Accessing non-final property ${leakingThisDescriptor.property.name} in constructor"
|
"Accessing non-final property ${leakingThisDescriptor.property.name} in constructor"
|
||||||
is NonFinalFunction ->
|
is NonFinalFunction ->
|
||||||
"Calling non-final function ${leakingThisDescriptor.function.name} in constructor"
|
"Calling non-final function ${leakingThisDescriptor.function.name} in constructor"
|
||||||
}
|
}
|
||||||
if (description != null) {
|
val memberDescriptorToFix = when (leakingThisDescriptor) {
|
||||||
holder.registerProblem(
|
is NonFinalProperty -> leakingThisDescriptor.property
|
||||||
expression, description,
|
is NonFinalFunction -> leakingThisDescriptor.function
|
||||||
when (leakingThisDescriptor) {
|
else -> null
|
||||||
is NonFinalProperty, is NonFinalFunction -> GENERIC_ERROR_OR_WARNING
|
|
||||||
else -> WEAK_WARNING
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
org.jetbrains.kotlin.idea.inspections.LeakingThisInspection
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// "Make 'x' final" "true"
|
||||||
|
|
||||||
|
open class My(open val x: Int) {
|
||||||
|
val y = <caret>x
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// "Make 'x' final" "true"
|
||||||
|
|
||||||
|
open class My(val x: Int) {
|
||||||
|
val y = x
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// "Make 'My' final" "true"
|
||||||
|
|
||||||
|
open class My(open val x: Int) {
|
||||||
|
val y = <caret>x
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// "Make 'My' final" "true"
|
||||||
|
|
||||||
|
class My(open val x: Int) {
|
||||||
|
val y = x
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Make 'x' final" "false"
|
||||||
|
// ACTION: Convert property initializer to getter
|
||||||
|
|
||||||
|
open class My(open val x: Int) {
|
||||||
|
val y = <caret>x
|
||||||
|
}
|
||||||
|
|
||||||
|
class Your(x : Int) : My(x) {
|
||||||
|
override val x: Int get() = 42
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Make 'init' final" "true"
|
||||||
|
|
||||||
|
open class My {
|
||||||
|
|
||||||
|
init {
|
||||||
|
<caret>init()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun init() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Make 'init' final" "true"
|
||||||
|
|
||||||
|
open class My {
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Make 'My' final" "true"
|
||||||
|
|
||||||
|
open class My {
|
||||||
|
|
||||||
|
init {
|
||||||
|
<caret>init()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun init() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Make 'My' final" "true"
|
||||||
|
|
||||||
|
class My {
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun init() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Make 'My' final" "false"
|
||||||
|
// ACTION: Add 'my =' to argument
|
||||||
|
|
||||||
|
abstract class My {
|
||||||
|
init {
|
||||||
|
register(<caret>this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(my: My) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// "Make 'My' final" "true"
|
||||||
|
|
||||||
|
open class My {
|
||||||
|
init {
|
||||||
|
register(<caret>this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(my: My) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// "Make 'My' final" "true"
|
||||||
|
|
||||||
|
class My {
|
||||||
|
init {
|
||||||
|
register(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(my: My) {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// "Make 'My' final" "false"
|
||||||
|
// ACTION: Add 'my =' to argument
|
||||||
|
|
||||||
|
open class My {
|
||||||
|
init {
|
||||||
|
register(<caret>this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : My()
|
||||||
|
|
||||||
|
fun register(my: My) {}
|
||||||
@@ -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")
|
@TestMetadata("idea/testData/quickfix/libraries")
|
||||||
@TestDataPath("$PROJECT_ROOT")
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
@RunWith(JUnit3RunnerWithInners.class)
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
|||||||
Reference in New Issue
Block a user