diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/DeprecationInspectionBase.java b/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/DeprecationInspectionBase.java index 94160be323bc..4591310073b2 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/DeprecationInspectionBase.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/DeprecationInspectionBase.java @@ -19,9 +19,7 @@ import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.daemon.JavaErrorMessages; import com.intellij.codeInsight.daemon.impl.analysis.HighlightMessageUtil; import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil; -import com.intellij.codeInspection.BaseJavaBatchLocalInspectionTool; -import com.intellij.codeInspection.ProblemHighlightType; -import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.codeInspection.*; import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.application.ApplicationManager; @@ -31,7 +29,14 @@ import com.intellij.psi.*; import com.intellij.psi.impl.JavaConstantExpressionEvaluator; import com.intellij.psi.impl.PsiImplUtil; import com.intellij.psi.infos.MethodCandidateInfo; +import com.intellij.psi.javadoc.PsiDocComment; +import com.intellij.psi.javadoc.PsiDocTag; +import com.intellij.psi.javadoc.PsiInlineDocTag; import com.intellij.psi.util.*; +import com.intellij.refactoring.util.RefactoringChangeUtil; +import com.intellij.util.ArrayUtil; +import com.intellij.util.ObjectUtils; +import one.util.streamex.MoreCollectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -275,7 +280,16 @@ abstract class DeprecationInspectionBase extends BaseJavaBatchLocalInspectionToo String description = JavaErrorMessages.message(forRemoval ? "marked.for.removal.symbol" : "deprecated.symbol", HighlightMessageUtil.getSymbolName(refElement, PsiSubstitutor.EMPTY)); - holder.registerProblem(elementToHighlight, getDescription(description, forRemoval, highlightType), highlightType, rangeInElement); + LocalQuickFix quickFix = null; + PsiMethodCallExpression methodCall = getMethodCall(elementToHighlight); + if (refElement instanceof PsiMethod && methodCall != null) { + PsiMethod replacement = findReplacementInJavaDoc((PsiMethod)refElement, methodCall); + if (replacement != null) { + quickFix = new ReplaceMethodCallFix((PsiMethodCallExpression)elementToHighlight.getParent().getParent(), replacement); + } + } + + holder.registerProblem(elementToHighlight, getDescription(description, forRemoval, highlightType), highlightType, rangeInElement, quickFix); } private static boolean isMarkedForRemoval(PsiModifierListOwner element, boolean forRemoval) { @@ -326,4 +340,80 @@ abstract class DeprecationInspectionBase extends BaseJavaBatchLocalInspectionToo } return description; } + + private static PsiMethod findReplacementInJavaDoc(@NotNull PsiMethod method, @NotNull PsiMethodCallExpression call) { + if (method instanceof PsiConstructorCall) return null; + PsiDocComment doc = method.getDocComment(); + if (doc == null) return null; + + PsiDocTag[] docTags = PsiTreeUtil.getChildrenOfType(doc, PsiInlineDocTag.class); + PsiDocTag[] tags = doc.getTags(); + PsiDocTag[] allTags = docTags == null ? tags : ArrayUtil.mergeArrays(docTags, tags); + if (allTags.length == 0) return null; + PsiMethod tagMethod = (PsiMethod)Arrays + .stream(allTags) + .filter(t -> { + String name = t.getName(); + return "link".equals(name) || "see".equals(name); + }) + .collect(MoreCollectors.onlyOne()) + .map(tag -> tag.getValueElement()) + .map(value -> value.getReference()) + .map(reference -> reference.resolve()) + .filter(resolved -> resolved instanceof PsiMethod) + .orElse(null); + return tagMethod == null || tagMethod.isDeprecated() || tagMethod.isEquivalentTo(method) || !areReplaceable(method, tagMethod, call) + ? null + : tagMethod; + } + + private static boolean areReplaceable(@NotNull PsiMethod initial, + @NotNull PsiMethod suggestedReplacement, + @NotNull PsiMethodCallExpression call) { + if (!PsiResolveHelper.SERVICE.getInstance(call.getProject()).isAccessible(suggestedReplacement, call, null)) { + return false; + } + + boolean isInitialStatic = initial.hasModifierProperty(PsiModifier.STATIC); + boolean isSuggestedStatic = suggestedReplacement.hasModifierProperty(PsiModifier.STATIC); + if (isInitialStatic && !isSuggestedStatic) { + return false; + } + if (!isInitialStatic && !isSuggestedStatic && !InheritanceUtil.isInheritorOrSelf(getQualifierClass(call), suggestedReplacement.getContainingClass(), true)) { + return false; + } + + String qualifierText; + if (isInitialStatic) { + qualifierText = ObjectUtils.notNull(suggestedReplacement.getContainingClass()).getQualifiedName() + "."; + } else { + PsiExpression qualifierExpression = call.getMethodExpression().getQualifierExpression(); + qualifierText = qualifierExpression == null ? "" : (qualifierExpression.getText() + "."); + } + + PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(initial.getProject()); + PsiExpressionList arguments = call.getArgumentList(); + PsiMethodCallExpression suggestedCall = (PsiMethodCallExpression)elementFactory + .createExpressionFromText(qualifierText + suggestedReplacement.getName() + arguments.getText(), call); + + MethodCandidateInfo result = ObjectUtils.tryCast(suggestedCall.resolveMethodGenerics(), MethodCandidateInfo.class); + return result != null && result.isApplicable(); + } + + @Nullable + private static PsiClass getQualifierClass(@NotNull PsiMethodCallExpression call) { + PsiExpression expression = call.getMethodExpression().getQualifierExpression(); + if (expression == null) { + return RefactoringChangeUtil.getThisClass(call); + } + return PsiUtil.resolveClassInType(expression.getType()); + } + + @Nullable + private static PsiMethodCallExpression getMethodCall(@NotNull PsiElement element) { + if (!(element instanceof PsiIdentifier)) return null; + PsiElement parent = element.getParent(); + if (!(parent instanceof PsiReferenceExpression)) return null; + return ObjectUtils.tryCast(parent.getParent(), PsiMethodCallExpression.class); + } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/ReplaceMethodCallFix.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/ReplaceMethodCallFix.kt new file mode 100644 index 000000000000..7e385254d4d8 --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/deprecation/ReplaceMethodCallFix.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2000-2017 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 com.intellij.codeInspection.deprecation + +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.util.PsiFormatUtil +import com.intellij.psi.util.PsiFormatUtilBase +import com.intellij.util.ObjectUtils +import org.jetbrains.annotations.Nls + +internal class ReplaceMethodCallFix(expr: PsiMethodCallExpression, replacementMethod: PsiMethod) : LocalQuickFixOnPsiElement(expr) { + private val myReplacementMethodPointer = + SmartPointerManager.getInstance(replacementMethod.project).createSmartPsiElementPointer(replacementMethod) + private val myReplacementText = + PsiFormatUtil.formatMethod(replacementMethod, PsiSubstitutor.EMPTY, PsiFormatUtilBase.SHOW_CONTAINING_CLASS or PsiFormatUtilBase.SHOW_NAME, 0) + + override fun getText(): String { + return "Replace method call with " + myReplacementText + } + + @Nls + override fun getFamilyName(): String { + return "Replace Method Call" + } + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val expr = ObjectUtils.tryCast(startElement, PsiMethodCallExpression::class.java) ?: return + val replacementMethod = myReplacementMethodPointer.element ?: return + + val qualifierExpression = expr.methodExpression.qualifierExpression + val isReplacementStatic = replacementMethod.hasModifierProperty(PsiModifier.STATIC) + val qualifierText = if (qualifierExpression != null && !isReplacementStatic) { + qualifierExpression.text + "." + } + else if (isReplacementStatic) { + replacementMethod.containingClass!!.qualifiedName!! + "." + } + else { + "" + } + + val elementFactory = JavaPsiFacade.getElementFactory(project) + val newMethodCall = elementFactory.createExpressionFromText(qualifierText + replacementMethod.name + expr.argumentList.text, expr) + val replaced = expr.replace(newMethodCall) as PsiMethodCallExpression + JavaCodeStyleManager.getInstance(project).shortenClassReferences(replaced.methodExpression) + } +} diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/afterGenerics.java b/java/java-tests/testData/inspection/deprecation/quickFix/afterGenerics.java new file mode 100644 index 000000000000..9fddcf2cc24f --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/afterGenerics.java @@ -0,0 +1,19 @@ +// "Replace method call with Test.nnn" "true" +class Test { + static void example() { + Test t = new Test<>(); + t.nnn(""); + } + + /** + * {@link Test#nnn(Object)} + * + */ + @Deprecated + void mmm(T t) { + } + + void nnn(T t) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/afterReplacementFromSuper.java b/java/java-tests/testData/inspection/deprecation/quickFix/afterReplacementFromSuper.java new file mode 100644 index 000000000000..b806d0e6a83d --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/afterReplacementFromSuper.java @@ -0,0 +1,23 @@ +// "Replace method call with Test2.nnn" "true" +class Test extends Test2 { + static void example() { + Test t = new Test(); + t.nnn(""); + } + + /** + * {@link Test2#nnn(String)} + * + */ + @Deprecated + void mmm(String t) { + } + + +} + +class Test2 { + void nnn(String t) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/afterSimple.java b/java/java-tests/testData/inspection/deprecation/quickFix/afterSimple.java new file mode 100644 index 000000000000..f79ce09d7170 --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/afterSimple.java @@ -0,0 +1,18 @@ +// "Replace method call with Test.nnn" "true" +public class Test { + + void foo() { + nnn(""); + } + + /** + * {@link Test#nnn(CharSequence)} + * + */ + @Deprecated + void mmm(String s) { + } + + void nnn(CharSequence ss) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/afterStaticReplacement.java b/java/java-tests/testData/inspection/deprecation/quickFix/afterStaticReplacement.java new file mode 100644 index 000000000000..0ade6689c6b2 --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/afterStaticReplacement.java @@ -0,0 +1,19 @@ +// "Replace method call with Test.mmm1" "true" +class Test { + static void example() { + Test t = new Test(); + Test.mmm1(""); + } + + /** + * {@link Test#mmm1(String)} + * + */ + @Deprecated + void mmm(String t) { + } + + static void mmm1(String string) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/beforeGenerics.java b/java/java-tests/testData/inspection/deprecation/quickFix/beforeGenerics.java new file mode 100644 index 000000000000..5fd0e590da3b --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/beforeGenerics.java @@ -0,0 +1,19 @@ +// "Replace method call with Test.nnn" "true" +class Test { + static void example() { + Test t = new Test<>(); + t.mmm(""); + } + + /** + * {@link Test#nnn(Object)} + * + */ + @Deprecated + void mmm(T t) { + } + + void nnn(T t) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/beforeReplacementFromSuper.java b/java/java-tests/testData/inspection/deprecation/quickFix/beforeReplacementFromSuper.java new file mode 100644 index 000000000000..f1251f3589e9 --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/beforeReplacementFromSuper.java @@ -0,0 +1,23 @@ +// "Replace method call with Test2.nnn" "true" +class Test extends Test2 { + static void example() { + Test t = new Test(); + t.mmm(""); + } + + /** + * {@link Test2#nnn(String)} + * + */ + @Deprecated + void mmm(String t) { + } + + +} + +class Test2 { + void nnn(String t) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/beforeReplacementNotAvailable.java b/java/java-tests/testData/inspection/deprecation/quickFix/beforeReplacementNotAvailable.java new file mode 100644 index 000000000000..b1354a6d4208 --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/beforeReplacementNotAvailable.java @@ -0,0 +1,23 @@ +// "Replace method call with Test2.nnn" "false" +class Test { + static void example() { + Test t = new Test(); + t.mmm(""); + } + + /** + * {@link Test2#nnn(String)} + * + */ + @Deprecated + void mmm(String t) { + } + + +} + +class Test2 { + void nnn(String t) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/beforeSimple.java b/java/java-tests/testData/inspection/deprecation/quickFix/beforeSimple.java new file mode 100644 index 000000000000..f987a57d0db8 --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/beforeSimple.java @@ -0,0 +1,18 @@ +// "Replace method call with Test.nnn" "true" +public class Test { + + void foo() { + mmm(""); + } + + /** + * {@link Test#nnn(CharSequence)} + * + */ + @Deprecated + void mmm(String s) { + } + + void nnn(CharSequence ss) { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/deprecation/quickFix/beforeStaticReplacement.java b/java/java-tests/testData/inspection/deprecation/quickFix/beforeStaticReplacement.java new file mode 100644 index 000000000000..6e09e04495ac --- /dev/null +++ b/java/java-tests/testData/inspection/deprecation/quickFix/beforeStaticReplacement.java @@ -0,0 +1,19 @@ +// "Replace method call with Test.mmm1" "true" +class Test { + static void example() { + Test t = new Test(); + t.mmm(""); + } + + /** + * {@link Test#mmm1(String)} + * + */ + @Deprecated + void mmm(String t) { + } + + static void mmm1(String string) { + + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/DeprecationInspectionFixTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/DeprecationInspectionFixTest.java new file mode 100644 index 000000000000..af501abaae4f --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/DeprecationInspectionFixTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2017 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 com.intellij.java.codeInspection; + +import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.deprecation.DeprecationInspection; +import org.jetbrains.annotations.NotNull; + +public class DeprecationInspectionFixTest extends LightQuickFixParameterizedTestCase { + @NotNull + @Override + protected LocalInspectionTool[] configureLocalInspectionTools() { + return new LocalInspectionTool[]{new DeprecationInspection()}; + } + + public void test() { doAllTests(); } + + @Override + protected String getBasePath() { + return "/inspection/deprecation/quickFix"; + } +}