diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77dd3b9d884..51f893cc632 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Currently the versioning policy of this project follows [Semantic Versioning v2.
- Fixed false positive RV_EXCEPTION_NOT_THROWN when asserting to exception throws ([[#2628](https://github.com/spotbugs/spotbugs/issues/2628)])
- Fix false positive CT_CONSTRUCTOR_THROW when supertype has final finalize ([[#2665](https://github.com/spotbugs/spotbugs/issues/2665)])
- Lowered the priority of `PA_PUBLIC_MUTABLE_OBJECT_ATTRIBUTE` bug ([[#2652](https://github.com/spotbugs/spotbugs/issues/2652)])
+- Fixed detector `ThrowingExceptions` by removing false positive reports, such as synthetic methods (lambdas), methods which inherited their exception specifications and methods which call throwing methods ([#2040](https://github.com/spotbugs/spotbugs/issues/2040))
### Build
- Fix deprecated GHA on '::set-output' by using GITHUB_OUTPUT ([[#2651](https://github.com/spotbugs/spotbugs/pull/2651)])
diff --git a/spotbugs-tests/src/test/java/edu/umd/cs/findbugs/detect/Issue2040Test.java b/spotbugs-tests/src/test/java/edu/umd/cs/findbugs/detect/Issue2040Test.java
new file mode 100644
index 00000000000..51f127a6d47
--- /dev/null
+++ b/spotbugs-tests/src/test/java/edu/umd/cs/findbugs/detect/Issue2040Test.java
@@ -0,0 +1,66 @@
+package edu.umd.cs.findbugs.detect;
+
+import edu.umd.cs.findbugs.AbstractIntegrationTest;
+import edu.umd.cs.findbugs.test.matcher.BugInstanceMatcher;
+import edu.umd.cs.findbugs.test.matcher.BugInstanceMatcherBuilder;
+import org.junit.jupiter.api.Test;
+
+import static edu.umd.cs.findbugs.test.CountMatcher.containsExactly;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+
+class Issue2040Test extends AbstractIntegrationTest {
+ @Test
+ void test() {
+ performAnalysis("ghIssues/issue2040/Base.class",
+ "ghIssues/issue2040/Derived.class",
+ "ghIssues/issue2040/GenericBase.class",
+ "ghIssues/issue2040/GenericDerived.class",
+ "ghIssues/issue2040/Interface.class",
+ "ghIssues/issue2040/Generic.class",
+ "ghIssues/issue2040/Caller.class");
+
+ assertNumOfBugs("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", 3);
+ assertREBugInMethod("Derived", "lambda$lambda2$1", 36);
+ assertREBugInMethod("Derived", "runtimeExThrownFromCatch", 97);
+ assertREBugInMethod("Derived", "runtimeExceptionThrowingMethod", 45);
+
+ assertNumOfBugs("THROWS_METHOD_THROWS_CLAUSE_THROWABLE", 4);
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_THROWABLE", "Base", "method");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_THROWABLE", "Derived", "throwableThrowingMethod");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_THROWABLE", "GenericBase", "method1");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_THROWABLE", "GenericBase", "method2");
+
+ assertNumOfBugs("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", 5);
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "Derived", "exceptionThrowingMethod");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "Derived", "exThrownFromCatch");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "GenericBase", "method");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "GenericBase", "method2");
+ assertBugInMethod("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "Interface", "iMethod");
+ }
+
+ private void assertNumOfBugs(String bugtype, int num) {
+ final BugInstanceMatcher bugTypeMatcher = new BugInstanceMatcherBuilder()
+ .bugType(bugtype).build();
+ assertThat(getBugCollection(), containsExactly(num, bugTypeMatcher));
+ }
+
+ private void assertBugInMethod(String bugType, String className, String method) {
+ final BugInstanceMatcher bugInstanceMatcher = new BugInstanceMatcherBuilder()
+ .bugType(bugType)
+ .inClass(className)
+ .inMethod(method)
+ .build();
+ assertThat(getBugCollection(), hasItem(bugInstanceMatcher));
+ }
+
+ private void assertREBugInMethod(String className, String method, int line) {
+ final BugInstanceMatcher bugInstanceMatcher = new BugInstanceMatcherBuilder()
+ .bugType("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION")
+ .inClass(className)
+ .inMethod(method)
+ .atLine(line)
+ .build();
+ assertThat(getBugCollection(), hasItem(bugInstanceMatcher));
+ }
+}
diff --git a/spotbugs/etc/findbugs.xml b/spotbugs/etc/findbugs.xml
index 7754486391e..39d8056567c 100644
--- a/spotbugs/etc/findbugs.xml
+++ b/spotbugs/etc/findbugs.xml
@@ -665,7 +665,7 @@
reports="SSD_DO_NOT_USE_INSTANCE_LOCK_ON_SHARED_STATIC_DATA"/>
-
diff --git a/spotbugs/etc/messages.xml b/spotbugs/etc/messages.xml
index 3d0d051e374..76be837fcbf 100644
--- a/spotbugs/etc/messages.xml
+++ b/spotbugs/etc/messages.xml
@@ -8817,8 +8817,8 @@ Using floating-point variables should not be used as loop counters, as they are
- Method lists Exception in its throws clause.
- Method lists Exception in its throws clause.
+ Method lists Exception in its throws clause, but it could be more specific.
+ Method lists Exception in its throws clause, but it could be more specific.
@@ -8834,8 +8834,8 @@ Using floating-point variables should not be used as loop counters, as they are
- Method lists Throwable in its throws clause.
- Method lists Throwable in its throws clause.
+ Method lists Throwable in its throws clause, but it could be more specific.
+ Method lists Throwable in its throws clause, but it could be more specific.
diff --git a/spotbugs/src/main/java/edu/umd/cs/findbugs/ba/SignatureParser.java b/spotbugs/src/main/java/edu/umd/cs/findbugs/ba/SignatureParser.java
index af04f897f8e..a805f1c1f4b 100644
--- a/spotbugs/src/main/java/edu/umd/cs/findbugs/ba/SignatureParser.java
+++ b/spotbugs/src/main/java/edu/umd/cs/findbugs/ba/SignatureParser.java
@@ -113,6 +113,7 @@ public String next() {
break;
case 'L':
+ case 'T':
int semi = signature.indexOf(';', index + 1);
if (semi < 0) {
throw new IllegalStateException("Invalid method signature: " + signature);
diff --git a/spotbugs/src/main/java/edu/umd/cs/findbugs/detect/ThrowingExceptions.java b/spotbugs/src/main/java/edu/umd/cs/findbugs/detect/ThrowingExceptions.java
index 1e84f1f8469..6e7aeb55acb 100644
--- a/spotbugs/src/main/java/edu/umd/cs/findbugs/detect/ThrowingExceptions.java
+++ b/spotbugs/src/main/java/edu/umd/cs/findbugs/detect/ThrowingExceptions.java
@@ -3,12 +3,24 @@
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.OpcodeStack;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.ba.AnalysisContext;
+import edu.umd.cs.findbugs.ba.SignatureParser;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
+import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import edu.umd.cs.findbugs.util.ClassName;
import org.apache.bcel.Const;
+import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ExceptionTable;
+import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
+import org.apache.commons.lang3.StringUtils;
public class ThrowingExceptions extends OpcodeStackDetector {
@@ -18,25 +30,71 @@ public ThrowingExceptions(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
+ private JavaClass clazz;
+ private String exceptionThrown = null;
+
+ @Override
+ public void visit(JavaClass obj) {
+ exceptionThrown = null;
+ clazz = obj;
+ }
+
@Override
public void visit(Method obj) {
+ exceptionThrown = null;
+
if (obj.isSynthetic()) {
return;
}
- ExceptionTable exceptionTable = obj.getExceptionTable();
- if (exceptionTable == null) {
- return;
+ Stream exceptionStream = null;
+
+ // If the method is generic or is a method of a generic class, then first check the generic signature to avoid detection
+ // of generic descendants of Exception or Throwable as Exception or Throwable itself.
+ String signature = obj.getGenericSignature();
+ String[] exceptions = null;
+ if (signature != null) {
+ exceptions = StringUtils.substringsBetween(signature, "^", ";");
+ if (exceptions != null) {
+ exceptionStream = Arrays.stream(exceptions)
+ .filter(s -> s.charAt(0) == 'L')
+ .map(s -> ClassName.toDottedClassName(s.substring(1)));
+ }
+ }
+
+ // If the method is not generic or it does not throw a generic exception then it has no exception specification in its generic
+ // signature.
+ if (signature == null || exceptions == null) {
+ ExceptionTable exceptionTable = obj.getExceptionTable();
+ if (exceptionTable != null) {
+ exceptionStream = Arrays.stream(exceptionTable.getExceptionNames());
+ }
}
- String[] exceptionNames = exceptionTable.getExceptionNames();
- for (String exception : exceptionNames) {
- if ("java.lang.Exception".equals(exception)) {
- reportBug("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", getXMethod());
- } else if ("java.lang.Throwable".equals(exception)) {
- reportBug("THROWS_METHOD_THROWS_CLAUSE_THROWABLE", getXMethod());
+ // If the method throws Throwable or Exception because its ancestor throws the same exception then ignore it.
+ if (exceptionStream != null) {
+ Optional exception = exceptionStream
+ .filter(s -> "java.lang.Exception".equals(s) || "java.lang.Throwable".equals(s))
+ .findAny();
+ if (exception.isPresent() && !parentThrows(obj, exception.get())) {
+ exceptionThrown = exception.get();
}
}
+
+ // If the method's body is empty then we report the bug immediately.
+ if (obj.getCode() == null && exceptionThrown != null) {
+ reportBug("java.lang.Exception".equals(exceptionThrown) ? "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION"
+ : "THROWS_METHOD_THROWS_CLAUSE_THROWABLE", getXMethod());
+ }
+ }
+
+ @Override
+ public void visitAfter(Code obj) {
+ // For methods with bodies we report the exception after checking their body.
+ if (exceptionThrown != null) {
+ reportBug("java.lang.Exception".equals(exceptionThrown) ? "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION"
+ : "THROWS_METHOD_THROWS_CLAUSE_THROWABLE", getXMethod());
+ }
}
@Override
@@ -45,13 +103,111 @@ public void sawOpcode(int seen) {
OpcodeStack.Item item = stack.getStackItem(0);
if (item != null) {
if ("Ljava/lang/RuntimeException;".equals(item.getSignature())) {
- reportBug("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", getXMethod());
+ bugReporter.reportBug(
+ new BugInstance(this, "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", LOW_PRIORITY)
+ .addClass(this)
+ .addMethod(getXMethod())
+ .addSourceLine(this));
}
}
+ } else if (exceptionThrown != null &&
+ (seen == Const.INVOKEVIRTUAL ||
+ seen == Const.INVOKEINTERFACE ||
+ seen == Const.INVOKESTATIC)) {
+
+ // If the method throws Throwable or Exception because it invokes another method throwing such
+ // exceptions then ignore this bug by resetting exceptionThrown to null.
+ XMethod calledMethod = getXMethodOperand();
+ if (calledMethod == null) {
+ return;
+ }
+
+ String[] thrownExceptions = calledMethod.getThrownExceptions();
+ if (thrownExceptions != null && Arrays.stream(thrownExceptions)
+ .map(ClassName::toDottedClassName)
+ .anyMatch(exceptionThrown::equals)) {
+ exceptionThrown = null;
+ }
+
}
}
private void reportBug(String bugName, XMethod method) {
- bugReporter.reportBug(new BugInstance(this, bugName, NORMAL_PRIORITY).addClass(this).addMethod(method));
+ bugReporter.reportBug(new BugInstance(this, bugName, LOW_PRIORITY).addClass(this).addMethod(method));
+ }
+
+ private boolean parentThrows(@NonNull Method method, @DottedClassName String exception) {
+ return parentThrows(clazz, method, exception);
+ }
+
+ private boolean parentThrows(@NonNull JavaClass clazz, @NonNull Method method, @DottedClassName String exception) {
+ JavaClass ancestor;
+ boolean throwsEx = false;
+ try {
+ ancestor = clazz.getSuperClass();
+ if (ancestor != null) {
+ Optional superMethod = Arrays.stream(ancestor.getMethods())
+ .filter(m -> method.getName().equals(m.getName()) && signatureMatches(method, m))
+ .findAny();
+ if (superMethod.isPresent()) {
+ throwsEx = Arrays.stream(superMethod.get().getExceptionTable().getExceptionNames())
+ .anyMatch(exception::equals);
+ } else {
+ throwsEx = parentThrows(ancestor, method, exception);
+ }
+ }
+
+ for (JavaClass intf : clazz.getInterfaces()) {
+ Optional superMethod = Arrays.stream(intf.getMethods())
+ .filter(m -> method.getName().equals(m.getName()) && signatureMatches(method, m))
+ .findAny();
+ if (superMethod.isPresent()) {
+ throwsEx |= Arrays.stream(superMethod.get().getExceptionTable().getExceptionNames())
+ .anyMatch(exception::equals);
+ } else {
+ throwsEx |= parentThrows(intf, method, exception);
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ AnalysisContext.reportMissingClass(e);
+ }
+ return throwsEx;
+ }
+
+ private boolean signatureMatches(Method child, Method parent) {
+ String genSig = parent.getGenericSignature();
+ if (genSig == null) {
+ return child.getSignature().equals(parent.getSignature());
+ }
+
+ String sig = child.getSignature();
+
+ SignatureParser genSP = new SignatureParser(genSig);
+ SignatureParser sp = new SignatureParser(sig);
+
+ if (genSP.getNumParameters() != sp.getNumParameters()) {
+ return false;
+ }
+
+ String[] gArgs = genSP.getArguments();
+ String[] args = sp.getArguments();
+
+ for (int i = 0; i < sp.getNumParameters(); ++i) {
+ if (gArgs[i].charAt(0) == 'T') {
+ if (args[i].charAt(0) != 'L') {
+ return false;
+ }
+ } else {
+ if (!gArgs[i].equals(args[i])) {
+ return false;
+ }
+ }
+ }
+
+ String gRet = genSP.getReturnTypeSignature();
+ String ret = sp.getReturnTypeSignature();
+
+ return (gRet.charAt(0) == 'T' && ret.charAt(0) == 'L') ||
+ gRet.equals(ret);
}
}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/Base.java b/spotbugsTestCases/src/java/ghIssues/issue2040/Base.java
new file mode 100644
index 00000000000..680d038eadb
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/Base.java
@@ -0,0 +1,7 @@
+package ghIssues.issue2040;
+
+public class Base {
+ public void method() throws Throwable {
+ throw new Throwable();
+ }
+}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/Caller.java b/spotbugsTestCases/src/java/ghIssues/issue2040/Caller.java
new file mode 100644
index 00000000000..854072adfc7
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/Caller.java
@@ -0,0 +1,8 @@
+package ghIssues.issue2040;
+
+public class Caller {
+ public void method() throws Throwable {
+ Base b = new Base();
+ b.method();
+ }
+}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/Derived.java b/spotbugsTestCases/src/java/ghIssues/issue2040/Derived.java
new file mode 100644
index 00000000000..70747b4f29e
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/Derived.java
@@ -0,0 +1,108 @@
+package ghIssues.issue2040;
+
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class Derived extends Base implements Interface, Callable {
+ @Override
+ public void method() throws Throwable {
+ super.method();
+ }
+
+ @Override
+ public void iMethod() throws Exception {
+ return;
+ }
+
+ public void genericMethod() throws T {
+
+ }
+
+ public Integer call() throws Exception {
+ return 0;
+ }
+
+ public void lambda() {
+ ExecutorService es = Executors.newCachedThreadPool();
+ es.submit(() -> {
+ return null;
+ });
+ }
+
+ public void lambda2() {
+ Arrays.asList(1,2).stream()
+ .forEach(i -> { throw new RuntimeException(); });
+ }
+
+ public void lambda3() {
+ Arrays.asList(1,2).stream()
+ .forEach(i -> runtimeExceptionThrowingMethod());
+ }
+
+ public void runtimeExceptionThrowingMethod() {
+ throw new RuntimeException();
+ }
+
+ public void exceptionThrowingMethod() throws Exception {
+ throw new Exception();
+ }
+
+ public void throwableThrowingMethod() throws Throwable {
+ throw new Throwable();
+ }
+
+ public void exceptionThrowingMethod2() throws Exception {
+ exceptionThrowingMethod();
+ throw new Exception();
+ }
+
+ public void exceptionThrowingMethod3(boolean a) throws Exception {
+ if (a) {
+ throw new Exception();
+ } else {
+ exceptionThrowingMethod();
+ }
+ }
+
+ public void runtimeExCaught() {
+ try {
+ runtimeExceptionThrowingMethod();
+ } catch (RuntimeException e) {
+ System.err.println("Exception caught, no error");
+ }
+ }
+
+ public void exCaught() {
+ try {
+ exceptionThrowingMethod();
+ } catch (Exception e) {
+ System.err.println("Exception caught, no error");
+ }
+ }
+
+ public void exCaughtAndThrown() throws Exception {
+ try {
+ exceptionThrowingMethod();
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+
+ public void runtimeExThrownFromCatch() {
+ try {
+ runtimeExceptionThrowingMethod();
+ } catch (RuntimeException e) {
+ throw e;
+ }
+ }
+
+ public void exThrownFromCatch() throws Exception {
+ try {
+ runtimeExceptionThrowingMethod();
+ } catch (RuntimeException e) {
+ throw new Exception(e);
+ }
+ }
+}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/Generic.java b/spotbugsTestCases/src/java/ghIssues/issue2040/Generic.java
new file mode 100644
index 00000000000..0c54c9da234
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/Generic.java
@@ -0,0 +1,6 @@
+package ghIssues.issue2040;
+
+public class Generic {
+ void method() throws E {
+ }
+}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/GenericBase.java b/spotbugsTestCases/src/java/ghIssues/issue2040/GenericBase.java
new file mode 100644
index 00000000000..c94e6d2fbea
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/GenericBase.java
@@ -0,0 +1,34 @@
+package ghIssues.issue2040;
+
+public class GenericBase {
+ public T method() throws Exception {
+ throw new Exception();
+ }
+
+ public void method1(int n) {
+ }
+
+ public void method1(T t) throws Throwable {
+ throw new Throwable();
+ }
+
+ public void method2(String s, T t) {
+ }
+
+ public void method2(Number n, T t) {
+ }
+
+ public void method2(int n, T t) throws Throwable {
+ throw new Throwable();
+ }
+
+ public void method2(T t, int n) {
+ }
+
+ public void method2(T t, Number n) {
+ }
+
+ public void method2(T t, String s) throws Exception {
+ throw new Exception();
+ }
+}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/GenericDerived.java b/spotbugsTestCases/src/java/ghIssues/issue2040/GenericDerived.java
new file mode 100644
index 00000000000..a698c96ab24
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/GenericDerived.java
@@ -0,0 +1,23 @@
+package ghIssues.issue2040;
+
+public class GenericDerived extends GenericBase {
+ @Override
+ public Byte method() throws Exception {
+ throw new Exception();
+ }
+
+ @Override
+ public void method1(Byte b) throws Throwable {
+ throw new Throwable();
+ }
+
+ @Override
+ public void method2(int n, Byte b) throws Throwable {
+ throw new Throwable();
+ }
+
+ @Override
+ public void method2(Byte b, String s) throws Exception {
+ throw new Exception();
+ }
+}
diff --git a/spotbugsTestCases/src/java/ghIssues/issue2040/Interface.java b/spotbugsTestCases/src/java/ghIssues/issue2040/Interface.java
new file mode 100644
index 00000000000..671a1383177
--- /dev/null
+++ b/spotbugsTestCases/src/java/ghIssues/issue2040/Interface.java
@@ -0,0 +1,5 @@
+package ghIssues.issue2040;
+
+public interface Interface {
+ public void iMethod() throws Exception;
+}