Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for Issue 2040 #2072

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0553cab
Fix for Issue 2040
May 24, 2022
191f62b
Fix for Issue 2040
May 24, 2022
0105170
Updated according to the comments of @KengoTODA
May 30, 2022
d236c57
Merge branch 'ThrowFix' of github.com:baloghadamsoftware/spotbugs int…
May 30, 2022
2ad845f
Fix Git merge issue
May 30, 2022
f56ce15
More test cases added & bugs fixed
May 31, 2022
fb51242
Merge branch 'master' into ThrowFix
May 31, 2022
418a3ec
Re-enable detector `ThrowingException` after the fix which removed th…
May 31, 2022
1c31e8b
Merge branch 'master' into ThrowFix
Aug 30, 2022
f385d14
Merge branch 'master' into ThrowFix
KengoTODA Sep 2, 2022
b7d838e
Updated according to the comments of @ThrawnCA
Sep 5, 2022
642abd7
Fixed typos
Sep 6, 2022
6d8d9c3
Merge branch 'master' into ThrowFix
Sep 6, 2022
7c36871
Merge branch 'master' into ThrowFix
KengoTODA Oct 11, 2022
53dada5
Merge branch 'master' into ThrowFix
baloghadamsoftware Apr 27, 2023
6a63f2b
CHANGELOG updated
baloghadamsoftware Apr 27, 2023
5f5e4a7
Merge with master and resolved CHANGELOG.md conflict
gtoison Aug 11, 2023
a5c3853
Merge branch 'master' into ThrowFix
JuditKnoll Aug 29, 2023
a99977e
Use ClassName.toDottedClassName instead of string replace
JuditKnoll Aug 29, 2023
af5f0a5
assert exact bug locations
JuditKnoll Aug 29, 2023
c940ea1
add new tests
JuditKnoll Aug 30, 2023
ce1e8ca
remove unnecessary comment
JuditKnoll Aug 31, 2023
897e822
add more testcases
JuditKnoll Aug 31, 2023
8d60e68
add linenumber to THROWS_METHOD_THROWS_RUNTIMEEXCEPTION
JuditKnoll Aug 31, 2023
7067aef
simplify tests
JuditKnoll Aug 31, 2023
63aa186
Extend the descriptions to THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTIO…
JuditKnoll Aug 31, 2023
96fb36b
Merge branch 'master' into ThrowFix
JuditKnoll Oct 3, 2023
c4efe23
Merge branch 'master' into ThrowFix
JuditKnoll Oct 12, 2023
b7f52a1
Merge fix
JuditKnoll Oct 12, 2023
c6cfca5
Merge branch 'ThrowFixBas' into ThrowFix
JuditKnoll Oct 12, 2023
d4146f7
Merge pull request #18 from JuditKnoll/ThrowFix
baloghadamsoftware Oct 19, 2023
77f8e11
Merge branch 'master' into ThrowFix
JuditKnoll Oct 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
2 changes: 1 addition & 1 deletion spotbugs/etc/findbugs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@
reports="SSD_DO_NOT_USE_INSTANCE_LOCK_ON_SHARED_STATIC_DATA"/>
<Detector class="edu.umd.cs.findbugs.detect.DontUseFloatsAsLoopCounters" speed="fast"
reports="FL_FLOATS_AS_LOOP_COUNTERS"/>
<Detector class="edu.umd.cs.findbugs.detect.ThrowingExceptions" speed="fast" disabled="true"
<Detector class="edu.umd.cs.findbugs.detect.ThrowingExceptions" speed="fast"
reports="THROWS_METHOD_THROWS_RUNTIMEEXCEPTION,THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION,THROWS_METHOD_THROWS_CLAUSE_THROWABLE" />
<Detector class="edu.umd.cs.findbugs.detect.PermissionsSuper"
reports="PERM_SUPER_NOT_CALLED_IN_GETPERMISSIONS"/>
Expand Down
8 changes: 4 additions & 4 deletions spotbugs/etc/messages.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8817,8 +8817,8 @@ Using floating-point variables should not be used as loop counters, as they are
</Details>
</BugPattern>
<BugPattern type="THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION">
<ShortDescription>Method lists Exception in its throws clause.</ShortDescription>
<LongDescription>Method lists Exception in its throws clause.</LongDescription>
<ShortDescription>Method lists Exception in its throws clause, but it could be more specific.</ShortDescription>
<LongDescription>Method lists Exception in its throws clause, but it could be more specific.</LongDescription>
<Details>
<![CDATA[
<p>
Expand All @@ -8834,8 +8834,8 @@ Using floating-point variables should not be used as loop counters, as they are
</Details>
</BugPattern>
<BugPattern type="THROWS_METHOD_THROWS_CLAUSE_THROWABLE">
<ShortDescription>Method lists Throwable in its throws clause.</ShortDescription>
<LongDescription>Method lists Throwable in its throws clause.</LongDescription>
<ShortDescription>Method lists Throwable in its throws clause, but it could be more specific.</ShortDescription>
<LongDescription>Method lists Throwable in its throws clause, but it could be more specific.</LongDescription>
<Details>
<![CDATA[
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<String> 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<String> 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
Expand All @@ -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;
baloghadamsoftware marked this conversation as resolved.
Show resolved Hide resolved
}

}
}

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<Method> 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<Method> 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);
}
}
7 changes: 7 additions & 0 deletions spotbugsTestCases/src/java/ghIssues/issue2040/Base.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ghIssues.issue2040;

public class Base {
public void method() throws Throwable {
throw new Throwable();
}
}
8 changes: 8 additions & 0 deletions spotbugsTestCases/src/java/ghIssues/issue2040/Caller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ghIssues.issue2040;

public class Caller {
public void method() throws Throwable {
Base b = new Base();
b.method();
}
}