Testing Custom FindBugs Detectors in Eclipse

12:15 PM 3 Comments

We are starting to use Sonar where I work, and I've been tasked with finding out how to write custom FindBugs rules.


First of all, that was pretty fun getting down into the byte code, it made me feel like a Java super-villain. Daniel Schneller has a very helpful post on creating your own FindBugs rule. With his help, you too can feel like a Java super-villain.


Anyway, the first thing that I noticed is that there wasn't really an easy way to debug through my detector. =[ Or, at least, I wasn't able to find one.


For my first stab, I decided to try creating a method that would take my detector class and a test class file to run the detector against.



public ProjectStats analyze(Class clazz, Detector detector)

Of course, I failed to remember that FindBugs analyzes class files, and so I would need to provide the location of the target directory. You need to do this with the maven plugin, so I'm not too worried about it:



public ProjectStats analyze(String filePath, Detector detector)

Finally, the bug reporting strategy is passed around as a parameter in most places, so I had to include the BugPattern and BugReporter in my signature:



public ProjectStats analyze(String filePath, Detector detector,
BugPattern bugPattern, BugReporter bugReporter)

Inside, it's a total mess of configuration. Hopefully, there is a way to clean it up:



public ProjectStats analyze(String filePath, Detector detector,
BugPattern bugPattern, BugReporter bugReporter)
throws CheckedAnalysisException, IOException, InterruptedException {
// internal to FindBugs, the code uses the Detector2 interface
Detector2 det = new DetectorToDetector2Adapter(detector);

// register the rules message
I18N.instance().registerBugPattern(bugPattern);

// a great deal of code to say
// 'analyze the files in this directory'
IClassFactory classFactory = ClassFactory.instance();
IClassPath classPath = classFactory.createClassPath();
IAnalysisCache analysisCache = classFactory
.createAnalysisCache(classPath, bugReporter);
Global.setAnalysisCacheForCurrentThread(analysisCache);
FindBugs2.registerBuiltInAnalysisEngines(analysisCache);
IClassPathBuilder builder = classFactory
.createClassPathBuilder(bugReporter);
ICodeBaseLocator locator = classFactory
.createFilesystemCodeBaseLocator(filePath);
builder.addCodeBase(locator, true);
builder.build(classPath, new NoOpFindBugsProgress());
List classesToAnalyze = builder.getAppClassList();
AnalysisCacheToAnalysisContextAdapter analysisContext =
new AnalysisCacheToAnalysisContextAdapter();
AnalysisContext.setCurrentAnalysisContext(analysisContext);

// finally, perform the analysis
for ( ClassDescriptor d : classesToAnalyse) {
det.visitClass(d);
}

// return the results
return bugReporter.getProjectStats();
}

Since I'm not particularly worried about the reporting, I just created a simple PrintStream bug reporter:



private static class PrintStreamBugReporter implements BugReporter {
private ProjectStats stats = new ProjectStats();
private PrintStream os;
private List observers =
new ArrayList();

public PrintStreamBugReporter(PrintStream os) {
this.os = os;
}

@Override
public void addObserver(BugReporterObserver arg0) {
observers.add(arg0);
}

@Override
public void finish() {
// TODO Auto-generated method stub

}

@Override
public ProjectStats getProjectStats() {
return stats;
}

@Override
public BugReporter getRealBugReporter() {
return this;
}

@Override
public void reportBug(BugInstance arg0) {
stats.addBug(arg0);
for ( BugReporterObserver observer : observers ) {
observer.reportBug(arg0);
}
os.println(arg0.getAbridgedMessage());
}

@Override
public void reportQueuedErrors() {
}

@Override
public void setErrorVerbosity(int arg0) {
}

@Override
public void setPriorityThreshold(int arg0) {
}

@Override
public void logError(String arg0) {
os.println(arg0.getBytes());
}

@Override
public void logError(String arg0, Throwable arg1) {
os.println(arg0.getBytes());
arg1.printStackTrace(os);
}

@Override
public void reportMissingClass(ClassNotFoundException arg0) {
arg0.printStackTrace(os);
}

@Override
public void reportMissingClass(ClassDescriptor arg0) {
os.println("Class not found: " + arg0);
}

@Override
public void reportSkippedAnalysis(MethodDescriptor arg0) {
os.println("Skipped Method: " + arg0);
}

@Override
public void observeClass(ClassDescriptor arg0) {
}


}

There you go. Now, I can debug my detectors in my IDE.

Josh Cummings

"I love to teach, as a painter loves to paint, as a singer loves to sing, as a musician loves to play" - William Lyon Phelps

3 comments:

  1. Hi Josh,

    Thanks for this, very useful stuff. I found that for some detectors, I needed a couple of extra initialisation bits and pieces before launching the analysis. I put a "DetectorTester" class with my changes here: http://github.com/rewbs/DefaultEncodingDetector/blob/master/test/junit/org/soal/findbugs/test/support/DetectorTester.java

    I also referenced this blog post in a presentation here: http://www.slideshare.net/rewbs/custom-findbugsdetectors

    Btw, it looks like the generics have been swallowed from the code in this blog post.

    Cheers,
    Robin

    ReplyDelete
  2. Glad to hear it helped! Thanks for the attribution.

    ReplyDelete
  3. Hi,

    Unfortunately it wasn't available for you at the time, but I have created a small library utility which allows test-driving a FindBugs custom detector.

    The code is packaged as a jar which you can add to your test classpath to assert the behaviour of a new detector in a unit test without having to run the whole FindBugs executable to see the results.

    The github project page is at: https://github.com/youdevise/test-driven-detectors4findbugs
    (see downloads section for a packaged jar, or maven info)

    A blog post describing usage of the utility is at:
    https://dev.youdevise.com/YDBlog/index.php/2011/03/11/developing-custom-findbugs-detectors-a-test-driven-approach

    Thoughts and feedback, and issues submitted through github are very welcome.

    Regards,
    Graham

    ReplyDelete