diff --git a/java/java-analysis-api/src/com/intellij/codeInspection/GlobalJavaBatchInspectionTool.java b/java/java-analysis-api/src/com/intellij/codeInspection/GlobalJavaBatchInspectionTool.java index ddb074f82fc5..20d22d6f1cfb 100644 --- a/java/java-analysis-api/src/com/intellij/codeInspection/GlobalJavaBatchInspectionTool.java +++ b/java/java-analysis-api/src/com/intellij/codeInspection/GlobalJavaBatchInspectionTool.java @@ -16,7 +16,6 @@ package com.intellij.codeInspection; -import com.intellij.analysis.AnalysisScope; import com.intellij.codeInspection.reference.RefManager; import org.jetbrains.annotations.NotNull; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationInspection.java b/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationInspection.java index 174641dc2286..052c1a88eb8b 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationInspection.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationInspection.java @@ -2,41 +2,39 @@ package com.intellij.codeInspection.unneededThrows; import com.intellij.analysis.AnalysisScope; -import com.intellij.codeInsight.ExceptionUtil; import com.intellij.codeInsight.FileModificationService; -import com.intellij.codeInsight.daemon.impl.quickfix.MethodThrowsFix; +import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil; import com.intellij.codeInspection.*; import com.intellij.codeInspection.reference.*; import com.intellij.codeInspection.ui.SingleCheckboxOptionsPanel; +import com.intellij.codeInspection.unneededThrows.RedundantThrowsDeclarationLocalInspection.ThrowRefType; import com.intellij.java.analysis.JavaAnalysisBundle; import com.intellij.lang.jvm.JvmModifier; import com.intellij.openapi.application.WriteAction; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Comparing; import com.intellij.psi.*; -import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.searches.OverridingMethodsSearch; import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.util.IncorrectOperationException; import com.intellij.util.ObjectUtils; import com.intellij.util.Query; import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.psiutils.CommentTracker; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.ArrayList; -import java.util.List; - -public class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspectionTool { - private static final Logger LOG = Logger.getInstance(RedundantThrowsDeclarationInspection.class); +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +public final class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspectionTool { public boolean IGNORE_ENTRY_POINTS = false; private final RedundantThrowsDeclarationLocalInspection myLocalInspection = new RedundantThrowsDeclarationLocalInspection(this); - @Nullable + @NotNull @Override public JComponent createOptionsPanel() { return new SingleCheckboxOptionsPanel(JavaAnalysisBundle.message("ignore.exceptions.thrown.by.entry.points.methods"), this, "IGNORE_ENTRY_POINTS"); @@ -48,80 +46,54 @@ public class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspect @NotNull InspectionManager manager, @NotNull GlobalInspectionContext globalContext, @NotNull ProblemDescriptionsProcessor processor) { - if (refEntity instanceof RefMethod) { - final RefMethod refMethod = (RefMethod)refEntity; - if (refMethod.isSyntheticJSP()) return null; + if (!(refEntity instanceof RefMethod)) return null; - // if (refMethod.hasSuperMethods()) return null; + final RefMethod refMethod = (RefMethod)refEntity; + if (refMethod.isSyntheticJSP()) return null; - if (IGNORE_ENTRY_POINTS && refMethod.isEntry()) return null; + if (IGNORE_ENTRY_POINTS && refMethod.isEntry()) return null; - PsiClass[] unThrown = refMethod.getUnThrownExceptions(); - if (unThrown == null) return null; + final PsiClass[] unThrown = refMethod.getUnThrownExceptions(); + if (unThrown == null) return null; - PsiElement psiMethod = refMethod.getPsiElement(); - if (!(psiMethod instanceof PsiMethod)) return null; + final PsiElement element = refMethod.getPsiElement(); + if (!(element instanceof PsiMethod)) return null; - if (((PsiMethod)psiMethod).hasModifier(JvmModifier.NATIVE)) return null; + final PsiMethod method = (PsiMethod)element; - PsiReferenceList list = ((PsiMethod)psiMethod).getThrowsList(); - PsiClassType[] throwsList = list.getReferencedTypes(); - PsiJavaCodeReferenceElement[] throwsRefs = list.getReferenceElements(); - List problems = null; + if (method.hasModifier(JvmModifier.NATIVE)) return null; + if (JavaHighlightUtil.isSerializationRelatedMethod(method, method.getContainingClass())) return null; - final PsiManager psiManager = psiMethod.getManager(); - for (int i = 0; i < throwsList.length; i++) { - final PsiClassType throwsType = throwsList[i]; - final String throwsClassName = throwsType.getClassName(); - final PsiJavaCodeReferenceElement throwsRef = throwsRefs[i]; - if (ExceptionUtil.isUncheckedException(throwsType)) continue; - if (declaredInRemotableMethod((PsiMethod)psiMethod, throwsType)) continue; + final Set unThrownSet = ContainerUtil.set(unThrown); - for (PsiClass s : unThrown) { - final PsiClass throwsResolvedType = throwsType.resolve(); - if (psiManager.areElementsEquivalent(s, throwsResolvedType)) { - if (problems == null) problems = new ArrayList<>(1); - - RefClass ownerClass = refMethod.getOwnerClass(); - if (refMethod.isAbstract() || ownerClass != null && ownerClass.isInterface()) { - problems.add(manager.createProblemDescriptor(throwsRef, JavaAnalysisBundle.message( - "inspection.redundant.throws.problem.descriptor", "#ref"), new MyQuickFix(processor, throwsClassName), ProblemHighlightType.LIKE_UNUSED_SYMBOL, - false)); - } - else if (!refMethod.getDerivedMethods().isEmpty()) { - problems.add(manager.createProblemDescriptor(throwsRef, JavaAnalysisBundle.message( - "inspection.redundant.throws.problem.descriptor1", "#ref"), new MyQuickFix(processor, throwsClassName), ProblemHighlightType.LIKE_UNUSED_SYMBOL, - false)); - } - else { - problems.add(manager.createProblemDescriptor(throwsRef, JavaAnalysisBundle.message( - "inspection.redundant.throws.problem.descriptor2", "#ref"), new MyQuickFix(processor, throwsClassName), ProblemHighlightType.LIKE_UNUSED_SYMBOL, - false)); - } - } - } - } - - if (problems != null) { - return problems.toArray(CommonProblemDescriptor.EMPTY_ARRAY); - } - } - - return null; + return RedundantThrowsDeclarationLocalInspection.getProblems(method, IGNORE_ENTRY_POINTS) + .filter(throwRefType -> unThrownSet.contains(throwRefType.getType().resolve())) + .map(throwRefType -> { + final PsiElement throwsRef = throwRefType.getReference(); + final String message = getMessage(refMethod); + final MyQuickFix fix = new MyQuickFix(processor, throwRefType.getType().getClassName(), IGNORE_ENTRY_POINTS); + return manager.createProblemDescriptor(throwsRef, message, fix, ProblemHighlightType.LIKE_UNUSED_SYMBOL, false); + }) + .toArray(CommonProblemDescriptor.EMPTY_ARRAY); } - private static boolean declaredInRemotableMethod(final PsiMethod psiMethod, final PsiClassType throwsType) { - if (!throwsType.equalsToText("java.rmi.RemoteException")) return false; - PsiClass aClass = psiMethod.getContainingClass(); - if (aClass == null) return false; - PsiClass remote = - JavaPsiFacade.getInstance(aClass.getProject()).findClass("java.rmi.Remote", GlobalSearchScope.allScope(aClass.getProject())); - return remote != null && aClass.isInheritor(remote, true); + @NotNull + private static String getMessage(@NotNull final RefMethod refMethod) { + final RefClass ownerClass = refMethod.getOwnerClass(); + if (refMethod.isAbstract() || ownerClass != null && ownerClass.isInterface()) { + return JavaAnalysisBundle.message("inspection.redundant.throws.problem.descriptor", "#ref"); + } + if (!refMethod.getDerivedMethods().isEmpty()) { + return JavaAnalysisBundle.message("inspection.redundant.throws.problem.descriptor1", "#ref"); + } + + return JavaAnalysisBundle.message("inspection.redundant.throws.problem.descriptor2", "#ref"); } @Override - protected boolean queryExternalUsagesRequests(@NotNull final RefManager manager, @NotNull final GlobalJavaInspectionContext globalContext, + protected boolean queryExternalUsagesRequests(@NotNull final RefManager manager, + @NotNull final GlobalJavaInspectionContext globalContext, @NotNull final ProblemDescriptionsProcessor processor) { manager.iterate(new RefJavaVisitor() { @Override public void visitElement(@NotNull RefEntity refEntity) { @@ -142,9 +114,9 @@ public class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspect } @Override - @Nullable - public QuickFix getQuickFix(String hint) { - return new MyQuickFix(null, hint); + @NotNull + public QuickFix getQuickFix(String hint) { + return new MyQuickFix(null, hint, IGNORE_ENTRY_POINTS); } @Override @@ -153,19 +125,21 @@ public class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspect return fix instanceof MyQuickFix ? ((MyQuickFix)fix).myHint : null; } - @Nullable + @NotNull @Override public RefGraphAnnotator getAnnotator(@NotNull RefManager refManager) { return new RedundantThrowsGraphAnnotator(refManager); } - private static class MyQuickFix implements LocalQuickFix { + private static final class MyQuickFix implements LocalQuickFix { private final ProblemDescriptionsProcessor myProcessor; private final String myHint; + private final boolean myIgnoreEntryPoints; - MyQuickFix(final ProblemDescriptionsProcessor processor, final String hint) { + MyQuickFix(final ProblemDescriptionsProcessor processor, final String hint, boolean ignoreEntryPoints) { myProcessor = processor; myHint = hint; + myIgnoreEntryPoints = ignoreEntryPoints; } @Override @@ -175,97 +149,81 @@ public class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspect } @Override - public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) { + final PsiMethod psiMethod; + final CommonProblemDescriptor[] problems; + final RefMethod refMethod; + if (myProcessor != null) { - RefElement refElement = (RefElement)myProcessor.getElement(descriptor); - if (refElement instanceof RefMethod && refElement.isValid()) { - RefMethod refMethod = (RefMethod)refElement; - final CommonProblemDescriptor[] problems = myProcessor.getDescriptions(refMethod); - if (problems != null) { - removeExcessiveThrows(refMethod, null, problems); - } - } + final RefEntity refElement = myProcessor.getElement(descriptor); + if (!(refElement instanceof RefMethod) || !refElement.isValid()) return; + + refMethod = (RefMethod)refElement; + psiMethod = ObjectUtils.tryCast(refMethod.getPsiElement(), PsiMethod.class); + + problems = myProcessor.getDescriptions(refMethod); } else { - final PsiMethod psiMethod = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiMethod.class); - if (psiMethod != null) { - removeExcessiveThrows(null, psiMethod, new CommonProblemDescriptor[]{descriptor}); - } + psiMethod = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiMethod.class); + if (psiMethod == null) return; + + refMethod = null; + problems = new CommonProblemDescriptor[]{descriptor}; } + + removeExcessiveThrows(refMethod, psiMethod, problems); } - private void removeExcessiveThrows(@Nullable RefMethod refMethod, @Nullable final PsiModifierListOwner element, final CommonProblemDescriptor[] problems) { - try { - @Nullable final PsiMethod psiMethod; - if (element == null) { - LOG.assertTrue(refMethod != null); - psiMethod = ObjectUtils.tryCast(refMethod.getPsiElement(), PsiMethod.class); - } - else { - psiMethod = (PsiMethod)element; - } - if (psiMethod == null) return; //invalid refMethod - final Project project = psiMethod.getProject(); - final PsiManager psiManager = PsiManager.getInstance(project); - final List refsToDelete = new ArrayList<>(); - for (CommonProblemDescriptor problem : problems) { - final PsiElement psiElement = ((ProblemDescriptor)problem).getPsiElement(); - if (psiElement instanceof PsiJavaCodeReferenceElement) { - final PsiJavaCodeReferenceElement classRef = (PsiJavaCodeReferenceElement)psiElement; - final PsiType psiType = JavaPsiFacade.getElementFactory(psiManager.getProject()).createType(classRef); - removeException(refMethod, psiType, refsToDelete, psiMethod); - } else { - final PsiReferenceList throwsList = psiMethod.getThrowsList(); - final PsiClassType[] classTypes = throwsList.getReferencedTypes(); - for (PsiClassType classType : classTypes) { - final String text = classType.getClassName(); - if (Comparing.strEqual(myHint, text)) { - removeException(refMethod, classType, refsToDelete, psiMethod); - break; - } - } - } - } + private void removeExcessiveThrows(@Nullable final RefMethod refMethod, + @Nullable final PsiMethod psiMethod, + final CommonProblemDescriptor @Nullable [] problems) { + if (psiMethod == null) return; + if (problems == null) return; - //check read-only status for derived methods - if (!FileModificationService.getInstance().preparePsiElementsForWrite(refsToDelete)) return; + final Project project = psiMethod.getProject(); + final PsiManager psiManager = PsiManager.getInstance(project); - WriteAction.run(() -> { - for (final PsiElement aRefsToDelete : refsToDelete) { - if (aRefsToDelete.isValid()) { - aRefsToDelete.delete(); - } - } - }); - } - catch (IncorrectOperationException e) { - LOG.error(e); - } + final PsiElementFactory factory = JavaPsiFacade.getElementFactory(psiManager.getProject()); + + final Set refsToDelete = Arrays.stream(problems) + .map(problem -> (ProblemDescriptor)problem) + .map(ProblemDescriptor::getPsiElement) + .filter(psiElement -> psiElement instanceof PsiJavaCodeReferenceElement) + .map(reference -> (PsiJavaCodeReferenceElement)reference) + .map(factory::createType) + .flatMap(psiClassType -> removeException(refMethod, psiClassType, psiMethod)) + .collect(Collectors.toSet()); + + //check read-only status for derived methods + if (!FileModificationService.getInstance().preparePsiElementsForWrite(refsToDelete)) return; + + WriteAction.run(() -> { + for (final PsiElement element : refsToDelete) { + new CommentTracker().deleteAndRestoreComments(element); + } + }); } - private void removeException(RefMethod refMethod, - PsiType exceptionType, - List refsToDelete, - PsiMethod psiMethod) { - ContainerUtil.addAll(refsToDelete, MethodThrowsFix.Remove.extractRefsToRemove(psiMethod, exceptionType)); + private StreamEx removeException(@Nullable final RefMethod refMethod, + @NotNull final PsiType exceptionType, + @NotNull final PsiMethod psiMethod) { + final StreamEx elements = RedundantThrowsDeclarationLocalInspection.getProblems(psiMethod, myIgnoreEntryPoints) + .filter(throwRefType -> exceptionType.isAssignableFrom(throwRefType.getType())) + .map(ThrowRefType::getReference); + final Stream tail; if (refMethod != null) { - assert myProcessor != null; + tail = refMethod.getDerivedMethods().stream() + .filter(refDerived -> refDerived.getPsiElement() instanceof PsiMethod) + .flatMap(refDerived -> removeException(refDerived, exceptionType, (PsiMethod)refDerived.getPsiElement())); - for (RefMethod refDerived : refMethod.getDerivedMethods()) { - PsiElement method = refDerived.getPsiElement(); - if (method instanceof PsiMethod) { - removeException(refDerived, exceptionType, refsToDelete, (PsiMethod)method); - } - } ProblemDescriptionsProcessor.resolveAllProblemsInElement(myProcessor, refMethod); } else { final Query query = OverridingMethodsSearch.search(psiMethod); - query.forEach(m -> { - removeException(null, exceptionType, refsToDelete, m); - return true; - }); + tail = query.findAll().stream() + .flatMap(method -> removeException(null, exceptionType, method)); } + return elements.append(tail); } @Override @@ -274,7 +232,7 @@ public class RedundantThrowsDeclarationInspection extends GlobalJavaBatchInspect } } - @Nullable + @NotNull @Override public LocalInspectionTool getSharedLocalInspectionTool() { return myLocalInspection; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationLocalInspection.java b/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationLocalInspection.java index 2933b97e80d9..6acc8017c9c9 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationLocalInspection.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/unneededThrows/RedundantThrowsDeclarationLocalInspection.java @@ -3,29 +3,46 @@ package com.intellij.codeInspection.unneededThrows; import com.intellij.codeInsight.ExceptionUtil; import com.intellij.codeInsight.daemon.JavaErrorBundle; +import com.intellij.codeInsight.daemon.QuickFixBundle; import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil; -import com.intellij.codeInsight.daemon.impl.quickfix.MethodThrowsFix; +import com.intellij.codeInsight.javadoc.JavaDocUtil; import com.intellij.codeInspection.*; import com.intellij.codeInspection.deadCode.UnusedDeclarationInspectionBase; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.java.analysis.JavaAnalysisBundle; +import com.intellij.lang.jvm.JvmModifier; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; +import com.intellij.psi.javadoc.PsiDocComment; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.ArrayUtil; import com.siyeh.ig.JavaOverridingMethodUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; +import com.siyeh.ig.psiutils.CommentTracker; +import com.siyeh.ig.psiutils.TypeUtils; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.*; -import java.util.*; +import java.util.Arrays; +import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.intellij.psi.PsiModifier.ABSTRACT; + @SuppressWarnings("InspectionDescriptionNotFoundInspection") // delegates -public class RedundantThrowsDeclarationLocalInspection extends AbstractBaseJavaLocalInspectionTool { - private final RedundantThrowsDeclarationInspection myGlobalTool; +public final class RedundantThrowsDeclarationLocalInspection extends AbstractBaseJavaLocalInspectionTool { + @NotNull private final RedundantThrowsDeclarationInspection myGlobalTool; @TestOnly - public RedundantThrowsDeclarationLocalInspection() {this(new RedundantThrowsDeclarationInspection());} + public RedundantThrowsDeclarationLocalInspection() { + this(new RedundantThrowsDeclarationInspection()); + } - public RedundantThrowsDeclarationLocalInspection(@NotNull RedundantThrowsDeclarationInspection tool) {myGlobalTool = tool;} + public RedundantThrowsDeclarationLocalInspection(@NotNull final RedundantThrowsDeclarationInspection tool) { + myGlobalTool = tool; + } @Override @NotNull @@ -40,84 +57,183 @@ public class RedundantThrowsDeclarationLocalInspection extends AbstractBaseJavaL } @Override - public ProblemDescriptor @Nullable [] checkMethod(@NotNull PsiMethod method, @NotNull InspectionManager manager, boolean isOnTheFly) { - return checkExceptionsNeverThrown(method, manager); + public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getDisplayName() { + return JavaAnalysisBundle.message("inspection.redundant.throws.display.name"); } - private ProblemDescriptor @Nullable [] checkExceptionsNeverThrown(PsiMethod method, - InspectionManager inspectionManager) { - if (method instanceof SyntheticElement) return null; - PsiClass containingClass = method.getContainingClass(); - if (containingClass == null || JavaHighlightUtil.isSerializationRelatedMethod(method, containingClass)) return null; + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, final boolean isOnTheFly) { + return new RedundantThrowsVisitor(holder, myGlobalTool.IGNORE_ENTRY_POINTS); + } - PsiCodeBlock body = method.getBody(); - if (body == null) return null; + @Contract(pure = true) + static StreamEx getProblems(@Nullable final PsiMethod method, final boolean ignoreEntryPoints) { + if (method == null) return StreamEx.empty(); + if (ignoreEntryPoints && UnusedDeclarationInspectionBase.isDeclaredAsEntryPoint(method)) return StreamEx.empty(); + if (method.hasModifier(JvmModifier.NATIVE)) return StreamEx.empty(); + if (JavaHighlightUtil.isSerializationRelatedMethod(method, method.getContainingClass())) return StreamEx.empty(); - if (myGlobalTool.IGNORE_ENTRY_POINTS && UnusedDeclarationInspectionBase.isDeclaredAsEntryPoint(method)) { - return null; - } + final PsiReferenceList throwsList = method.getThrowsList(); + final StreamEx redundantInThrowsList = StreamEx.zip(throwsList.getReferenceElements(), + throwsList.getReferencedTypes(), + ThrowRefType::new); - ReferenceAndType[] thrownExceptions = getThrownCheckedExceptions(method); - if (thrownExceptions.length == 0) return null; - - PsiModifierList modifierList = method.getModifierList(); - boolean needCheckOverridingMethods = !(modifierList.hasModifierProperty(PsiModifier.PRIVATE) || - modifierList.hasModifierProperty(PsiModifier.STATIC) || - modifierList.hasModifierProperty(PsiModifier.FINAL) || - method.isConstructor() || - containingClass instanceof PsiAnonymousClass || - containingClass.hasModifierProperty(PsiModifier.FINAL)); - Collection unhandled = RedundantThrowsGraphAnnotator.getUnhandledExceptions(body, method, containingClass); - List candidates = Arrays.stream(thrownExceptions) - .filter(refAndType -> unhandled.stream().noneMatch(unhandledException -> unhandledException.isAssignableFrom(refAndType.type) || refAndType.type.isAssignableFrom(unhandledException))) - .collect(Collectors.toList()); - - if (candidates.isEmpty()) return null; - if (needCheckOverridingMethods) { - Predicate methodContainsThrownExceptions = m -> m.getThrowsList().getReferencedTypes().length != 0; - Stream overridingMethods = JavaOverridingMethodUtil.getOverridingMethodsIfCheapEnough(method, null, methodContainsThrownExceptions); - if (overridingMethods == null) return null; - - Iterator overridingMethodIt = overridingMethods.iterator(); - while (overridingMethodIt.hasNext()) { - PsiMethod m = overridingMethodIt.next(); - PsiClassType[] overridingMethodThrownExceptions = m.getThrowsList().getReferencedTypes(); - - candidates.removeIf(refAndType -> { - PsiClassType type = refAndType.type; - return Arrays.stream(overridingMethodThrownExceptions).anyMatch(type::isAssignableFrom); + final PsiDocComment comment = method.getDocComment(); + final Stream redundantInJavadoc; + if (comment != null) { + redundantInJavadoc = Arrays.stream(comment.getTags()) + .filter(tag -> "throws".equals(tag.getName())) + .flatMap(tag -> { + final PsiClass aClass = JavaDocUtil.resolveClassInTagValue(tag.getValueElement()); + if (aClass == null) return Stream.empty(); + return Stream.of(new ThrowRefType(tag, TypeUtils.getType(aClass))); }); - - if (candidates.isEmpty()) return null; - } + } + else { + redundantInJavadoc = Stream.empty(); } - return candidates.stream().map(exceptionType -> { - PsiJavaCodeReferenceElement reference = exceptionType.ref; - String description = JavaErrorBundle.message("exception.is.never.thrown", JavaHighlightUtil.formatType(exceptionType.type)); - LocalQuickFix quickFix = new MethodThrowsFix.Remove(method, exceptionType.type, false); - return inspectionManager.createProblemDescriptor(reference, description, quickFix, ProblemHighlightType.LIKE_UNUSED_SYMBOL, true); - }).toArray(ProblemDescriptor[]::new); + return redundantInThrowsList.append(redundantInJavadoc) + .filter(ThrowRefType::isCheckedException) + .filter(p -> !p.isRemoteExceptionInRemoteMethod(method)); } - private static ReferenceAndType[] getThrownCheckedExceptions(PsiMethod method) { - return Stream - .of(method.getThrowsList().getReferenceElements()) - .map(ref -> { - PsiElement resolved = ref.resolve(); - return resolved instanceof PsiClass && !ExceptionUtil.isUncheckedException((PsiClass)resolved) ? new ReferenceAndType(ref) : null; - }) - .filter(Objects::nonNull) - .toArray(ReferenceAndType[]::new); + private static final class RedundantThrowsVisitor extends JavaElementVisitor { + + private @NotNull final ProblemsHolder myHolder; + private final boolean myIgnoreEntryPoints; + private RedundantThrowsVisitor(@NotNull final ProblemsHolder holder, final boolean ignoreEntryPoints) { + myHolder = holder; + myIgnoreEntryPoints = ignoreEntryPoints; + } + + @Override + public void visitMethod(@NotNull final PsiMethod method) { + getProblems(method, myIgnoreEntryPoints) + .filter(throwRefType -> !throwRefType.isThrownIn(method)) + .filter(throwRefType -> !throwRefType.isInOverridenOf(method)) + .forEach(throwRefType -> { + final PsiElement reference = throwRefType.myReference; + final PsiClassType exceptionType = throwRefType.myType; + + final String description = JavaErrorBundle.message("exception.is.never.thrown", JavaHighlightUtil.formatType(exceptionType)); + final RedundantThrowsQuickFix fix = new RedundantThrowsQuickFix(exceptionType.getCanonicalText(), method.getName()); + myHolder.registerProblem(reference, description, ProblemHighlightType.LIKE_UNUSED_SYMBOL, fix); + }); + } + + private static final class RedundantThrowsQuickFix implements LocalQuickFix { + + @NotNull private final String myMethodName; + @NotNull private final String myExceptionName; + private RedundantThrowsQuickFix(@NotNull final String exceptionName, @NotNull final String methodName) { + myExceptionName = exceptionName; + myMethodName = methodName; + } + + @Override + public @IntentionName @NotNull String getName() { + final String exceptionName = StringUtil.getShortName(myExceptionName); + return QuickFixBundle.message("fix.throws.list.remove.exception", exceptionName, myMethodName); + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return QuickFixBundle.message("fix.throws.list.family"); + } + + @Override + public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) { + final PsiElement elem = descriptor.getPsiElement(); + + final CommentTracker ct = new CommentTracker(); + ct.deleteAndRestoreComments(elem); + } + + } } - private static final class ReferenceAndType { - private final PsiJavaCodeReferenceElement ref; - private final PsiClassType type; + static final class ThrowRefType { + @NotNull private final PsiElement myReference; + @NotNull private final PsiClassType myType; - private ReferenceAndType(@NotNull PsiJavaCodeReferenceElement ref) { - this.ref = ref; - type = JavaPsiFacade.getElementFactory(ref.getProject()).createType(ref); + ThrowRefType(@NotNull final PsiElement reference, @NotNull final PsiClassType type) { + myReference = reference; + myType = type; + } + + private boolean isCheckedException() { + return !ExceptionUtil.isUncheckedException(myType); + } + + private boolean isRemoteExceptionInRemoteMethod(@NotNull final PsiMethod psiMethod) { + if (!myType.equalsToText("java.rmi.RemoteException")) return false; + + final PsiClass containingClass = psiMethod.getContainingClass(); + if (containingClass == null) return false; + + final JavaPsiFacade instance = JavaPsiFacade.getInstance(containingClass.getProject()); + final PsiClass remote = instance.findClass("java.rmi.Remote", GlobalSearchScope.allScope(containingClass.getProject())); + return remote != null && containingClass.isInheritor(remote, true); + } + + @Contract(pure = true) + private boolean isInOverridenOf(@NotNull final PsiMethod method) { + if (!isMethodPossiblyOverriden(method)) return false; + + final Predicate methodContainsThrownExceptions = m -> !ArrayUtil.isEmpty(m.getThrowsList().getReferencedTypes()); + + final Stream overridingMethods = JavaOverridingMethodUtil.getOverridingMethodsIfCheapEnough(method, + null, + methodContainsThrownExceptions); + if (overridingMethods == null) return true; + + return overridingMethods.anyMatch(m -> isThrownIn(m) || isInThrowsListOf(m)); + } + + @Contract(pure = true) + private boolean isThrownIn(@NotNull final PsiMethod method) { + if (method.hasModifierProperty(ABSTRACT)) return true; + final PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) return true; + + if (JavaHighlightUtil.isSerializationRelatedMethod(method, containingClass)) return true; + + final PsiCodeBlock body = method.getBody(); + if (body == null) return true; + + final Set unhandled = RedundantThrowsGraphAnnotator.getUnhandledExceptions(body, method, containingClass); + + return unhandled.stream().anyMatch(myType::isAssignableFrom); + } + + @Contract(pure = true) + private boolean isInThrowsListOf(@NotNull final PsiMethod method) { + return Arrays.stream(method.getThrowsList().getReferencedTypes()) + .anyMatch(myType::isAssignableFrom); + } + + @Contract(pure = true) + private static boolean isMethodPossiblyOverriden(@NotNull final PsiMethod method) { + final PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) return false; + + final PsiModifierList modifierList = method.getModifierList(); + + return !(modifierList.hasModifierProperty(PsiModifier.PRIVATE) || + modifierList.hasModifierProperty(PsiModifier.STATIC) || + modifierList.hasModifierProperty(PsiModifier.FINAL) || + method.isConstructor() || + containingClass instanceof PsiAnonymousClass || + containingClass.hasModifierProperty(PsiModifier.FINAL)); + } + + @NotNull PsiElement getReference() { + return myReference; + } + + @NotNull PsiClassType getType() { + return myType; } } } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/afterJavadoc.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/afterJavadoc.java index 59939b180189..5a65d855b8ce 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/afterJavadoc.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/afterJavadoc.java @@ -1,4 +1,4 @@ -// "Remove 'IOException' from 'foo' throws list" "true" +// "Fix all 'Redundant 'throws' clause' problems in file" "true" import java.io.*; class A { diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/beforeJavadoc.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/beforeJavadoc.java index 945acf9524a5..55e2939a3f57 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/beforeJavadoc.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/methodThrows/beforeJavadoc.java @@ -1,4 +1,4 @@ -// "Remove 'IOException' from 'foo' throws list" "true" +// "Fix all 'Redundant 'throws' clause' problems in file" "true" import java.io.*; class A {