From 58e4ff05021d663754b511864d4c09386a232634 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Thu, 21 May 2015 17:06:59 +0200 Subject: [PATCH] Add quickfixes for unnecessary casts and nullability operations to cleanup inspection. Rewrite two quickfixes from Java to Kotlin. Add inspection description. Use marker interface for identifying quickfixes applicable for code cleanup. --- .../KotlinDeprecation.html | 7 + idea/src/META-INF/plugin.xml | 2 +- .../inspections/KotlinCleanupInspection.kt | 53 +++--- .../kotlin/idea/quickfix/CleanupFix.kt | 25 +++ .../quickfix/DeprecatedAnnotationSyntaxFix.kt | 2 +- .../DeprecatedEnumEntryDelimiterSyntaxFix.kt | 4 +- ...catedEnumEntrySuperConstructorSyntaxFix.kt | 4 +- .../quickfix/DeprecatedLambdaSyntaxFix.kt | 2 +- .../idea/quickfix/DeprecatedTraitSyntaxFix.kt | 2 +- .../kotlin/idea/quickfix/ExclExclCallFix.java | 179 ------------------ .../kotlin/idea/quickfix/ExclExlCallFixes.kt | 99 ++++++++++ .../quickfix/MissingConstructorKeywordFix.kt | 2 +- .../idea/quickfix/QuickFixRegistrar.java | 8 +- .../RemoveNameFromFunctionExpressionFix.kt | 2 +- .../RemoveRightPartOfBinaryExpressionFix.java | 2 +- .../kotlin/idea/quickfix/ReplaceCallFix.java | 101 ---------- .../kotlin/idea/quickfix/ReplaceCallFix.kt | 83 ++++++++ .../quickfix/ReplaceObsoleteLabelSyntaxFix.kt | 2 +- ...ReplaceJavaClassAsAnnotationArgumentFix.kt | 2 +- ...eplaceJavaClassAsAnnotationParameterFix.kt | 13 +- idea/testData/inspections/cleanup/cleanup.kt | 12 ++ .../inspections/cleanup/cleanup.kt.after | 12 ++ 22 files changed, 285 insertions(+), 333 deletions(-) create mode 100644 idea/resources/inspectionDescriptions/KotlinDeprecation.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt delete mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExclCallFix.java create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExlCallFixes.kt delete mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.java create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt diff --git a/idea/resources/inspectionDescriptions/KotlinDeprecation.html b/idea/resources/inspectionDescriptions/KotlinDeprecation.html new file mode 100644 index 00000000000..8a523b9e534 --- /dev/null +++ b/idea/resources/inspectionDescriptions/KotlinDeprecation.html @@ -0,0 +1,7 @@ + + +This inspection is used during the code cleanup operation (Analyze | Code Cleanup) to automatically +replace usages of obsolete language features or unnecessarily verbose code constructs with +compact and up-to-date syntax. + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 38c9a88c3da..9bcf437c988 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1009,7 +1009,7 @@ ? { if (isOnTheFly || file !is JetFile || !ProjectRootsUtil.isInProjectSource(file)) { @@ -55,29 +56,32 @@ public class KotlinCleanupInspection(): LocalInspectionTool(), CleanupLocalInspe file.acceptChildren(object: JetTreeVisitorVoid() { override fun visitElement(element: PsiElement) { super.visitElement(element) - val collection = diagnostics.forElement(element) - collection.forEach { - if (it.isCleanup()) { - problemDescriptors.add(it.toProblemDescriptor(file, manager)) - } - } + diagnostics.forElement(element) + .filter { it.isCleanup() } + .map { it.toProblemDescriptor(file, manager) } + .filterNotNullTo(problemDescriptors) } }) return problemDescriptors.toTypedArray() } - private fun Diagnostic.isCleanup() = getFactory().isCleanup() || isObsoleteLabel() + private fun Diagnostic.isCleanup() = getFactory() in cleanupDiagnosticsFactories || isObsoleteLabel() - private fun DiagnosticFactory<*>.isCleanup() = - this == Errors.DEPRECATED_TRAIT_KEYWORD || - this == Errors.DEPRECATED_ANNOTATION_SYNTAX || - this == Errors.ENUM_ENTRY_USES_DEPRECATED_OR_NO_DELIMITER || - this == Errors.ENUM_ENTRY_USES_DEPRECATED_SUPER_CONSTRUCTOR || - this == Errors.DEPRECATED_LAMBDA_SYNTAX || - this == Errors.MISSING_CONSTRUCTOR_KEYWORD || - this == Errors.FUNCTION_EXPRESSION_WITH_NAME || - this == Errors.JAVA_LANG_CLASS_PARAMETER_IN_ANNOTATION || - this == ErrorsJvm.JAVA_LANG_CLASS_ARGUMENT_IN_ANNOTATION + private val cleanupDiagnosticsFactories = setOf( + Errors.DEPRECATED_TRAIT_KEYWORD, + Errors.DEPRECATED_ANNOTATION_SYNTAX, + Errors.ENUM_ENTRY_USES_DEPRECATED_OR_NO_DELIMITER, + Errors.ENUM_ENTRY_USES_DEPRECATED_SUPER_CONSTRUCTOR, + Errors.DEPRECATED_LAMBDA_SYNTAX, + Errors.MISSING_CONSTRUCTOR_KEYWORD, + Errors.FUNCTION_EXPRESSION_WITH_NAME, + Errors.JAVA_LANG_CLASS_PARAMETER_IN_ANNOTATION, + ErrorsJvm.JAVA_LANG_CLASS_ARGUMENT_IN_ANNOTATION, + Errors.UNNECESSARY_NOT_NULL_ASSERTION, + Errors.UNNECESSARY_SAFE_CALL, + Errors.USELESS_CAST, + Errors.USELESS_ELVIS + ) private fun Diagnostic.isObsoleteLabel(): Boolean { val annotationEntry = getPsiElement().getNonStrictParentOfType() ?: return false @@ -86,20 +90,15 @@ public class KotlinCleanupInspection(): LocalInspectionTool(), CleanupLocalInspe private fun Diagnostic.toProblemDescriptor(file: JetFile, manager: InspectionManager): ProblemDescriptor? { val quickFixes = JetPsiChecker.createQuickfixes(this) - .filter { it.isCleanupFix(this) } + .filter { it is CleanupFix } .map { IntentionWrapper(it, file) } - return manager.createProblemDescriptor(getPsiElement(), + if (quickFixes.isEmpty()) return null + + return manager.createProblemDescriptor(getPsiElement(), DefaultErrorMessages.render(this), false, quickFixes.toTypedArray(), ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } - - private fun IntentionAction.isCleanupFix(diagnostic: Diagnostic): Boolean { - if (diagnostic.getFactory() == Errors.UNRESOLVED_REFERENCE) { - return this is ReplaceObsoleteLabelSyntaxFix - } - return this !is JetWholeProjectModalAction<*> - } } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt new file mode 100644 index 00000000000..a7f8ff22a00 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2010-2015 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.quickfix + +/** + * Marker interface for quickfixes that can be used as part of the "Cleanup Code" action. The diagnostics + * that produce these quickfixes need to be added to KotlinCleanupInspection.cleanupDiagnosticsFactories. + */ +public interface CleanupFix +// TODO(yole): add isSafeToApply() method here to get rid of filtering by diagnostics factories in +// KotlinCleanupInspection diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedAnnotationSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedAnnotationSyntaxFix.kt index d6961d98557..f2de071e753 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedAnnotationSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedAnnotationSyntaxFix.kt @@ -30,7 +30,7 @@ import org.jetbrains.kotlin.psi.JetFile import org.jetbrains.kotlin.psi.JetPrimaryConstructor import org.jetbrains.kotlin.psi.JetPsiFactory -public class DeprecatedAnnotationSyntaxFix(element: JetAnnotation) : JetIntentionAction(element) { +public class DeprecatedAnnotationSyntaxFix(element: JetAnnotation) : JetIntentionAction(element), CleanupFix { override fun getFamilyName(): String = "Replace with '@' annotations" override fun getText(): String = "Replace with '@' annotations" diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntryDelimiterSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntryDelimiterSyntaxFix.kt index 9e5e834a923..1ab5085abf1 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntryDelimiterSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntryDelimiterSyntaxFix.kt @@ -32,7 +32,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComme import org.jetbrains.kotlin.psi.stubs.elements.JetStubElementTypes import org.jetbrains.kotlin.resolve.DeclarationsChecker -class DeprecatedEnumEntryDelimiterSyntaxFix(element: JetEnumEntry): JetIntentionAction(element) { +class DeprecatedEnumEntryDelimiterSyntaxFix(element: JetEnumEntry): JetIntentionAction(element), CleanupFix { override fun getFamilyName(): String = getText() @@ -41,7 +41,7 @@ class DeprecatedEnumEntryDelimiterSyntaxFix(element: JetEnumEntry): JetIntention override fun invoke(project: Project, editor: Editor?, file: JetFile?) = insertLackingCommaSemicolon(element) override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean - = super.isAvailable(project, editor, file) && DeclarationsChecker.enumEntryUsesDeprecatedOrNoDelimiter(element) + = super.isAvailable(project, editor, file) && DeclarationsChecker.enumEntryUsesDeprecatedOrNoDelimiter(element) companion object : JetSingleIntentionActionFactory() { override fun createAction(diagnostic: Diagnostic): IntentionAction? = diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntrySuperConstructorSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntrySuperConstructorSyntaxFix.kt index c906db972c1..2bd1a8f3c5c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntrySuperConstructorSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedEnumEntrySuperConstructorSyntaxFix.kt @@ -36,7 +36,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComme import org.jetbrains.kotlin.psi.stubs.elements.JetStubElementTypes import org.jetbrains.kotlin.resolve.DeclarationsChecker -class DeprecatedEnumEntrySuperConstructorSyntaxFix(element: JetEnumEntry): JetIntentionAction(element) { +class DeprecatedEnumEntrySuperConstructorSyntaxFix(element: JetEnumEntry): JetIntentionAction(element), CleanupFix { override fun getFamilyName(): String = getText() override fun getText(): String = "Change to short enum entry super constructor" @@ -44,7 +44,7 @@ class DeprecatedEnumEntrySuperConstructorSyntaxFix(element: JetEnumEntry): JetIn override fun invoke(project: Project, editor: Editor?, file: JetFile?) = changeConstructorToShort(element) override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean - = super.isAvailable(project, editor, file) && DeclarationsChecker.enumEntryUsesDeprecatedSuperConstructor(element) + = super.isAvailable(project, editor, file) && DeclarationsChecker.enumEntryUsesDeprecatedSuperConstructor(element) companion object: JetSingleIntentionActionFactory() { override fun createAction(diagnostic: Diagnostic): IntentionAction? = diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt index f5b4cb43942..1c76dfd043c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt @@ -35,7 +35,7 @@ import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.utils.sure import java.util.ArrayList -public class DeprecatedLambdaSyntaxFix(element: JetFunctionLiteralExpression) : JetIntentionAction(element) { +public class DeprecatedLambdaSyntaxFix(element: JetFunctionLiteralExpression) : JetIntentionAction(element), CleanupFix { override fun getText() = JetBundle.message("migrate.lambda.syntax") override fun getFamilyName() = JetBundle.message("migrate.lambda.syntax.family") diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedTraitSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedTraitSyntaxFix.kt index 3ad5f8cd3cb..7dd3eb1f256 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedTraitSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedTraitSyntaxFix.kt @@ -27,7 +27,7 @@ import org.jetbrains.kotlin.psi.JetClass import org.jetbrains.kotlin.psi.JetFile import org.jetbrains.kotlin.psi.JetPsiFactory -public class DeprecatedTraitSyntaxFix(element: PsiElement): JetIntentionAction(element) { +public class DeprecatedTraitSyntaxFix(element: PsiElement): JetIntentionAction(element), CleanupFix { override fun getFamilyName() = "Replace with 'interface'" override fun getText() = getFamilyName() diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExclCallFix.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExclCallFix.java deleted file mode 100644 index 97c3d2aa7f7..00000000000 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExclCallFix.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2010-2015 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.quickfix; - -import com.intellij.codeInsight.FileModificationService; -import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.impl.source.tree.LeafPsiElement; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.util.IncorrectOperationException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.idea.JetBundle; -import org.jetbrains.kotlin.lexer.JetTokens; -import org.jetbrains.kotlin.psi.JetExpression; -import org.jetbrains.kotlin.psi.JetFile; -import org.jetbrains.kotlin.psi.JetPostfixExpression; -import org.jetbrains.kotlin.psi.JetPsiFactory; - -import static org.jetbrains.kotlin.psi.PsiPackage.JetPsiFactory; - -@SuppressWarnings("IntentionDescriptionNotFoundInspection") -public class ExclExclCallFix implements IntentionAction { - - private final boolean isRemove; - - private ExclExclCallFix(boolean remove) { - isRemove = remove; - } - - public static ExclExclCallFix removeExclExclCall() { - return new ExclExclCallFix(true); - } - - public static ExclExclCallFix introduceExclExclCall() { - return new ExclExclCallFix(false); - } - - @NotNull - @Override - public String getText() { - return isRemove ? JetBundle.message("remove.unnecessary.non.null.assertion") : JetBundle.message("introduce.non.null.assertion"); - } - - @NotNull - @Override - public String getFamilyName() { - return getText(); - } - - @Override - public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { - if (file instanceof JetFile) { - if (!isRemove) { - return isAvailableForIntroduce(editor, file); - } - else { - return isAvailableForRemove(editor, file); - } - } - - return false; - } - - @Override - public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { - if (!FileModificationService.getInstance().prepareFileForWrite(file)) { - return; - } - - JetPsiFactory psiFactory = JetPsiFactory(project); - if (!isRemove) { - JetExpression modifiedExpression = getExpressionForIntroduceCall(editor, file); - JetExpression exclExclExpression = psiFactory.createExpression(modifiedExpression.getText() + "!!"); - modifiedExpression.replace(exclExclExpression); - } - else { - JetPostfixExpression postfixExpression = getExclExclPostfixExpression(editor, file); - JetExpression expression = psiFactory.createExpression(postfixExpression.getBaseExpression().getText()); - postfixExpression.replace(expression); - } - } - - @Override - public boolean startInWriteAction() { - return true; - } - - private static boolean isAvailableForIntroduce(Editor editor, PsiFile file) { - return getExpressionForIntroduceCall(editor, file) != null; - } - - private static boolean isAvailableForRemove(Editor editor, PsiFile file) { - return getExclExclPostfixExpression(editor, file) != null; - } - - private static PsiElement getExclExclElement(Editor editor, PsiFile file) { - PsiElement elementAtCaret = file.findElementAt(editor.getCaretModel().getOffset()); - if (isExclExclLeaf(elementAtCaret)) { - return elementAtCaret; - } - - if (elementAtCaret != null) { - // Case when caret is placed right after !! - PsiElement prevLeaf = PsiTreeUtil.prevLeaf(elementAtCaret); - if (isExclExclLeaf(prevLeaf)) { - return prevLeaf; - } - } - - return null; - } - - private static boolean isExclExclLeaf(@Nullable PsiElement element) { - return (element instanceof LeafPsiElement) && ((LeafPsiElement) element).getElementType() == JetTokens.EXCLEXCL; - } - - private static JetExpression getExpressionForIntroduceCall(Editor editor, PsiFile file) { - PsiElement elementAtCaret = file.findElementAt(editor.getCaretModel().getOffset()); - if (elementAtCaret != null) { - JetExpression expression = getExpressionForIntroduceCall(elementAtCaret); - if (expression != null) { - return expression; - } - - // Maybe caret is after error element - expression = getExpressionForIntroduceCall(PsiTreeUtil.prevLeaf(elementAtCaret)); - if (expression != null) { - return expression; - } - } - - return null; - } - - private static JetExpression getExpressionForIntroduceCall(PsiElement problemElement) { - if (problemElement instanceof LeafPsiElement && ((LeafPsiElement) problemElement).getElementType() == JetTokens.DOT) { - PsiElement sibling = problemElement.getPrevSibling(); - if (sibling instanceof JetExpression) { - return (JetExpression) sibling; - } - } - - return null; - } - - private static JetPostfixExpression getExclExclPostfixExpression(Editor editor, PsiFile file) { - PsiElement exclExclElement = getExclExclElement(editor, file); - - if (exclExclElement != null) { - PsiElement parent = exclExclElement.getParent(); - if (parent != null) { - PsiElement operationParent = parent.getParent(); - if (operationParent instanceof JetPostfixExpression && ((JetPostfixExpression) operationParent).getBaseExpression() != null) { - return (JetPostfixExpression) operationParent; - } - } - } - - return null; - } -} diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExlCallFixes.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExlCallFixes.kt new file mode 100644 index 00000000000..b94d10f4ab7 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ExclExlCallFixes.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2015 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.quickfix + +import com.intellij.codeInsight.FileModificationService +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.JetBundle +import org.jetbrains.kotlin.lexer.JetTokens +import org.jetbrains.kotlin.psi.JetExpression +import org.jetbrains.kotlin.psi.JetFile +import org.jetbrains.kotlin.psi.JetPostfixExpression +import org.jetbrains.kotlin.psi.JetPsiFactory + +public abstract class ExclExclCallFix : IntentionAction { + override fun getFamilyName(): String = getText() + + override fun startInWriteAction(): Boolean = true + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile) = file is JetFile +} + +public class RemoveExclExclCallFix(val psiElement: PsiElement) : ExclExclCallFix(), CleanupFix { + override fun getText(): String = JetBundle.message("remove.unnecessary.non.null.assertion") + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean + = super.isAvailable(project, editor, file) && + getExclExclPostfixExpression() != null + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + if (!FileModificationService.getInstance().prepareFileForWrite(file)) return + + val postfixExpression = getExclExclPostfixExpression() ?: return + val expression = JetPsiFactory(project).createExpression(postfixExpression.getBaseExpression().getText()) + postfixExpression.replace(expression) + } + + private fun getExclExclPostfixExpression(): JetPostfixExpression? { + val operationParent = psiElement.getParent() + if (operationParent is JetPostfixExpression && operationParent.getBaseExpression() != null) { + return operationParent + } + return null + } + + companion object : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction + = RemoveExclExclCallFix(diagnostic.getPsiElement()) + } +} + +public class AddExclExclCallFix(val psiElement: PsiElement) : ExclExclCallFix() { + override fun getText() = JetBundle.message("introduce.non.null.assertion") + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean + = super.isAvailable(project, editor, file) && + getExpressionForIntroduceCall() != null + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + val modifiedExpression = getExpressionForIntroduceCall() ?: return + val exclExclExpression = JetPsiFactory(project).createExpression(modifiedExpression.getText() + "!!") + modifiedExpression.replace(exclExclExpression) + } + + protected fun getExpressionForIntroduceCall(): JetExpression? { + if (psiElement is LeafPsiElement && psiElement.getElementType() == JetTokens.DOT) { + val sibling = psiElement.getPrevSibling() + if (sibling is JetExpression) { + return sibling + } + } + + return null + } + + companion object : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction + = AddExclExclCallFix(diagnostic.getPsiElement()) + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/MissingConstructorKeywordFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/MissingConstructorKeywordFix.kt index b06e40aabac..81576bece2f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/MissingConstructorKeywordFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/MissingConstructorKeywordFix.kt @@ -26,7 +26,7 @@ import org.jetbrains.kotlin.psi.JetFile import org.jetbrains.kotlin.psi.JetPrimaryConstructor import org.jetbrains.kotlin.idea.quickfix.quickfixUtil.addConstructorKeyword -public class MissingConstructorKeywordFix(element: JetPrimaryConstructor) : JetIntentionAction(element) { +public class MissingConstructorKeywordFix(element: JetPrimaryConstructor) : JetIntentionAction(element), CleanupFix { override fun getFamilyName(): String = getText() override fun getText(): String = "Add 'constructor' keyword" diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java index aa82d5bf88b..62971ea8b8a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java @@ -173,11 +173,11 @@ public class QuickFixRegistrar { QuickFixes.factories.put(UNUSED_VARIABLE, RemovePsiElementSimpleFix.createRemoveVariableFactory()); - QuickFixes.actions.put(UNNECESSARY_SAFE_CALL, ReplaceCallFix.toDotCallFromSafeCall()); - QuickFixes.actions.put(UNSAFE_CALL, ReplaceCallFix.toSafeCall()); + QuickFixes.factories.put(UNNECESSARY_SAFE_CALL, ReplaceWithDotCallFix.Companion); + QuickFixes.factories.put(UNSAFE_CALL, ReplaceWithSafeCallFix.Companion); - QuickFixes.actions.put(UNSAFE_CALL, ExclExclCallFix.introduceExclExclCall()); - QuickFixes.actions.put(UNNECESSARY_NOT_NULL_ASSERTION, ExclExclCallFix.removeExclExclCall()); + QuickFixes.factories.put(UNSAFE_CALL, AddExclExclCallFix.Companion); + QuickFixes.factories.put(UNNECESSARY_NOT_NULL_ASSERTION, RemoveExclExclCallFix.Companion); QuickFixes.factories.put(UNSAFE_INFIX_CALL, ReplaceInfixCallFix.createFactory()); JetSingleIntentionActionFactory removeProtectedModifierFactory = RemoveModifierFix.createRemoveModifierFromListOwnerFactory(PROTECTED_KEYWORD); diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveNameFromFunctionExpressionFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveNameFromFunctionExpressionFix.kt index 18bd5f19594..b8dd55a8093 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveNameFromFunctionExpressionFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveNameFromFunctionExpressionFix.kt @@ -30,7 +30,7 @@ import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantsOfType import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments import org.jetbrains.kotlin.resolve.BindingContext -public class RemoveNameFromFunctionExpressionFix(element: JetNamedFunction) : JetIntentionAction(element) { +public class RemoveNameFromFunctionExpressionFix(element: JetNamedFunction) : JetIntentionAction(element), CleanupFix { override fun getText(): String = "Remove identifier from function expression" override fun getFamilyName(): String = getText() diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveRightPartOfBinaryExpressionFix.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveRightPartOfBinaryExpressionFix.java index ff3c2b355f7..175c34e2a3b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveRightPartOfBinaryExpressionFix.java +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/RemoveRightPartOfBinaryExpressionFix.java @@ -26,7 +26,7 @@ import org.jetbrains.kotlin.idea.JetBundle; import org.jetbrains.kotlin.idea.core.quickfix.QuickFixUtil; import org.jetbrains.kotlin.psi.*; -public class RemoveRightPartOfBinaryExpressionFix extends JetIntentionAction { +public class RemoveRightPartOfBinaryExpressionFix extends JetIntentionAction implements CleanupFix { private final String message; public RemoveRightPartOfBinaryExpressionFix(@NotNull T element, String message) { diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.java deleted file mode 100644 index 038a4c95f4f..00000000000 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2010-2015 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.quickfix; - -import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.util.IncorrectOperationException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.kotlin.idea.JetBundle; -import org.jetbrains.kotlin.psi.*; - -import static org.jetbrains.kotlin.psi.PsiPackage.JetPsiFactory; - -public class ReplaceCallFix implements IntentionAction { - private final boolean toSafe; - - private ReplaceCallFix(boolean safe) { - this.toSafe = safe; - } - - /** - * @return quickfix for replacing dot call with toSafe (?.) call - */ - public static ReplaceCallFix toSafeCall() { - return new ReplaceCallFix(true); - } - - /** - * @return quickfix for replacing unnecessary toSafe (?.) call with dot call - */ - public static ReplaceCallFix toDotCallFromSafeCall() { - return new ReplaceCallFix(false); - } - - @NotNull - @Override - public String getText() { - return toSafe ? JetBundle.message("replace.with.safe.call") : JetBundle.message("replace.with.dot.call"); - } - - @NotNull - @Override - public String getFamilyName() { - return getText(); - } - - @Override - public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { - if (file instanceof JetFile) { - return getCallExpression(editor, (JetFile) file) != null; - } - return false; - } - - @Override - public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { - JetQualifiedExpression callExpression = getCallExpression(editor, (JetFile) file); - assert callExpression != null; - - JetExpression selector = callExpression.getSelectorExpression(); - if (selector != null) { - JetQualifiedExpression newElement = (JetQualifiedExpression) JetPsiFactory(callExpression).createExpression( - callExpression.getReceiverExpression().getText() + (toSafe ? "?." : ".") + selector.getText() - ); - - callExpression.replace(newElement); - } - } - - @Override - public boolean startInWriteAction() { - return true; - } - - private JetQualifiedExpression getCallExpression(@NotNull Editor editor, @NotNull JetFile file) { - PsiElement elementAtCaret = getElementAtCaret(editor, file); - return PsiTreeUtil.getParentOfType(elementAtCaret, toSafe ? JetDotQualifiedExpression.class : JetSafeQualifiedExpression.class); - } - - private static PsiElement getElementAtCaret(Editor editor, PsiFile file) { - return file.findElementAt(editor.getCaretModel().getOffset()); - } -} diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt new file mode 100644 index 00000000000..9f166884ff4 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2015 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.quickfix + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.extapi.psi.ASTDelegatePsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.JetBundle +import org.jetbrains.kotlin.psi.* + +public abstract class ReplaceCallFix protected (val psiElement: PsiElement) : IntentionAction { + + override fun getFamilyName(): String = getText() + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean { + if (file is JetFile) { + return getCallExpression() != null + } + return false + } + + override fun invoke(project: Project, editor: Editor?, file: PsiFile) { + val callExpression = getCallExpression() ?: return + + val selector = callExpression.getSelectorExpression() + if (selector != null) { + val newElement = JetPsiFactory(callExpression).createExpression( + callExpression.getReceiverExpression().getText() + operation + selector.getText()) as JetQualifiedExpression + + callExpression.replace(newElement) + } + } + + override fun startInWriteAction(): Boolean = true + + private fun getCallExpression(): JetQualifiedExpression? { + return PsiTreeUtil.getParentOfType(psiElement, classToReplace) + } + + abstract val operation: String + abstract val classToReplace: Class +} + +public class ReplaceWithSafeCallFix(psiElement: PsiElement): ReplaceCallFix(psiElement) { + override fun getText(): String = JetBundle.message("replace.with.safe.call") + override val operation: String get() = "?." + override val classToReplace: Class get() = javaClass() + + companion object : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction + = ReplaceWithSafeCallFix(diagnostic.getPsiElement()) + } +} + +public class ReplaceWithDotCallFix(psiElement: PsiElement): ReplaceCallFix(psiElement), CleanupFix { + override fun getText(): String = JetBundle.message("replace.with.dot.call") + override val operation: String get() = "." + override val classToReplace: Class get() = javaClass() + + companion object : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction + = ReplaceWithDotCallFix(diagnostic.getPsiElement()) + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt index fdda73e9a46..1c64b66948b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt @@ -30,7 +30,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComme import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType import org.jetbrains.kotlin.resolve.BindingContext -public class ReplaceObsoleteLabelSyntaxFix(element: JetAnnotationEntry?) : JetIntentionAction(element) { +public class ReplaceObsoleteLabelSyntaxFix(element: JetAnnotationEntry?) : JetIntentionAction(element), CleanupFix { override fun getFamilyName(): String = "Update obsolete label syntax" override fun getText(): String = "Replace with label ${element.getCalleeExpression()?.getText() ?: ""}@" diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationArgumentFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationArgumentFix.kt index e8bcdcdca46..3b21f88168c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationArgumentFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationArgumentFix.kt @@ -33,7 +33,7 @@ import java.util.ArrayList public class ReplaceJavaClassAsAnnotationArgumentFix( annotationEntry: JetAnnotationEntry -) : JetIntentionAction(annotationEntry) { +) : JetIntentionAction(annotationEntry), CleanupFix { override fun getText() = JetBundle.message("replace.java.class.argument") override fun getFamilyName() = JetBundle.message("replace.java.class.argument.family") diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationParameterFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationParameterFix.kt index 125e8585853..720e8ad9ffa 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationParameterFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/replaceJavaClass/ReplaceJavaClassAsAnnotationParameterFix.kt @@ -16,27 +16,22 @@ package org.jetbrains.kotlin.idea.quickfix.replaceJavaClass -import com.intellij.codeInsight.intention.IntentionAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.idea.JetBundle +import org.jetbrains.kotlin.idea.quickfix.CleanupFix import org.jetbrains.kotlin.idea.quickfix.JetIntentionAction import org.jetbrains.kotlin.idea.quickfix.JetSingleIntentionActionFactory import org.jetbrains.kotlin.idea.quickfix.JetWholeProjectForEachElementOfTypeFix -import org.jetbrains.kotlin.idea.quickfix.JetWholeProjectModalAction import org.jetbrains.kotlin.idea.quickfix.quickfixUtil.createIntentionFactory import org.jetbrains.kotlin.idea.quickfix.quickfixUtil.createIntentionForFirstParentOfType -import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers -import org.jetbrains.kotlin.idea.util.ShortenReferences -import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType -import java.util.ArrayList +import org.jetbrains.kotlin.psi.JetClass +import org.jetbrains.kotlin.psi.JetFile public class ReplaceJavaClassAsAnnotationParameterFix( annotationClass: JetClass -) : JetIntentionAction(annotationClass) { +) : JetIntentionAction(annotationClass), CleanupFix { override fun getText() = JetBundle.message("replace.java.class.parameter") override fun getFamilyName() = JetBundle.message("replace.java.class.parameter.family") diff --git a/idea/testData/inspections/cleanup/cleanup.kt b/idea/testData/inspections/cleanup/cleanup.kt index 9cef11cddd8..9f8535b26c5 100644 --- a/idea/testData/inspections/cleanup/cleanup.kt +++ b/idea/testData/inspections/cleanup/cleanup.kt @@ -30,3 +30,15 @@ fun foo() { continue@loop } } + +fun unnecessarySafeCall(x: String) { + x?.length() +} + +fun unnecessaryExclExcl(x: String) { + x!!.length() +} + +fun unnecessaryCast(x: String) = x as String + +fun unnecessaryElvis(x: String) = x ?: "" diff --git a/idea/testData/inspections/cleanup/cleanup.kt.after b/idea/testData/inspections/cleanup/cleanup.kt.after index 4ae57e05f26..b1ae42ea09d 100644 --- a/idea/testData/inspections/cleanup/cleanup.kt.after +++ b/idea/testData/inspections/cleanup/cleanup.kt.after @@ -32,3 +32,15 @@ fun foo() { continue@loop } } + +fun unnecessarySafeCall(x: String) { + x.length() +} + +fun unnecessaryExclExcl(x: String) { + x.length() +} + +fun unnecessaryCast(x: String) = x + +fun unnecessaryElvis(x: String) = x