Posted by
Gregg Sporar
on
Monday, February 16, 2009
A common question I hear is: "How can I integrate Code Collaborator with a static analysis tool?" The answer lies in the command line tool that is part of Code Collaborator: ccollab. It provides a wide array of features, including two that are key for this type of integration:
- extract detailed information about a code review
- add a comment (or defect) to a code review
To demonstrate, I created a little contrived example in Java:
1 package process.sub;
2
3 public class Two {
4 public static void methodTwo( int n )
5 {
6 int contrived = 0;
7 contrived = contrived = 5;
8 contrived = contrived++;
9 System.out.println(contrived);
10 }
11 }
12
FindBugs will report some problems with this code. Among other things, it will complain about the double assignment of a field on line 7.
To pull results from another tool into a Code Collaborator review, it helps if that tool can produce output that is easy to parse. With FindBugs, that is no problem: I used its -xml:withMessages flag. And it produced an XML file that contained (among other entries):
<BugInstance type="SA_LOCAL_DOUBLE_ASSIGNMENT" priority="2" abbrev="SA"
category="CORRECTNESS" instanceHash="21c3ddb607c3eff5f35d3f55b87f0454"
instanceOccurrenceNum="0" instanceOccurrenceMax="0">
<ShortMessage>Double assignment of local variable </ShortMessage>
<LongMessage>Double assignment of contrived in process.sub.Two.methodTwo(int)</LongMessage>
<Class classname="process.sub.Two" primary="true">
<SourceLine classname="process.sub.Two" start="3" end="10"
sourcefile="Two.java" sourcepath="process/sub/Two.java">
<Message>At Two.java:[lines 3-10]</Message>
</SourceLine>
<Message>In class process.sub.Two</Message>
</Class>
<Method classname="process.sub.Two" name="methodTwo"
signature="(I)V" isStatic="true" primary="true">
<SourceLine classname="process.sub.Two" start="6" end="10"
startBytecode="0" endBytecode="86" sourcefile="Two.java"
sourcepath="process/sub/Two.java"/>
<Message>In method process.sub.Two.methodTwo(int)</Message>
</Method>
<LocalVariable name="notUsed" register="1" pc="5" role="LOCAL_VARIABLE_NAMED">
<Message>Local variable named contrived</Message>
</LocalVariable>
<SourceLine classname="process.sub.Two" primary="true" start="7" end="7"
startBytecode="5" endBytecode="5" sourcefile="Two.java"
sourcepath="process/sub/Two.java">
<Message>At Two.java:[line 7]</Message>
</SourceLine>
</BugInstance>
To make this usable by Code Collaborator I wrote a little script that takes two parameters:
- a review number
- the XML output from FindBugs
After running my script, my review had comments added to it:
The script could be extended to also run FindBugs instead of using existing FindBugs output, but FindBugs works best when it is run across an entire project. Since a code review will typically not include all files in a project, it felt to me that FindBugs should be run separately.
Another environment-dependent issue has to do with file paths. Depending upon how they are invoked, FindBugs and Code Collaborator might not use the exact same path to describe the location of a source file. So in the script I sort of punt on that issue and use the base file name only, without the path. An environment-dependent fix could be put in place for that.
While this is written in Groovy for use with FindBugs, the concepts in it could be applied in any scripting language to any static analysis tool that produces parsable output:
if (args.length < 2) {
println "Usage: ParseFB <reviewId> <xml file from FindBugs>"
return
}
reviewId = args[0]
fbXMLFile = args[1]
try {
fbXML = new XmlSlurper().parse(new File(fbXMLFile))
}
catch( ex ) {
println "Unable to open: $fbXMLFile - ${ex.class.name} : ${ex.message}"
return
}
ccCmd = "ccollab"
ccURL = "http://localhost:6803"
ccUser = "findbugs"
ccPw = "\"\""
ccOptions = "--url $ccURL --user $ccUser --password $ccPw --quiet --non-interactive"
command = """ $ccCmd $ccOptions admin review-xml $reviewId
--xpath //reviews/review/artifacts/artifact/path """
outStream = new StringBuffer()
errStream = new StringBuffer()
proc = command.execute()
proc.consumeProcessOutput(outStream, errStream)
proc.waitFor()
if (proc.exitValue() != 0) {
println "Error code from ccollab command: ${ proc.exitValue()}"
println "Error: $errStream"
if (outStream.length() > 0)
println "Standard output: $outStream"
return
}
filesInReview = outStream.toString().split("</path>")
filesInReview.each {
if (it.toLowerCase().endsWith(".java"))
putInComments(it)
}
return
def putInComments (file) {
components = file.split("/")
fbFileName = components[components.length-1]
components = file.split("<path>")
ccFileName = components[components.length-1]
entries = fbXML.BugInstance.findAll {
it.Class.SourceLine.@sourcefile.toString() == fbFileName
}
entries.each {
lineNumber = it.SourceLine.@start.toString()
commentText = it.ShortMessage.toString()
command = """ $ccCmd $ccOptions admin review comment create --file
$ccFileName --line-number $lineNumber $reviewId
\"$commentText\" """
println "Processing: $command"
outStream = new StringBuffer()
errStream = new StringBuffer()
proc = command.execute()
proc.consumeProcessOutput(outStream, errStream)
proc.waitFor()
if (proc.exitValue() != 0) {
println "Error code from ccollab command: ${ proc.exitValue()}"
println "Error: $errStream"
if (outStream.length() > 0)
println "Standard output: $outStream"
return
}
}
}