Skip to content

Commit

Permalink
CFG Visualizer (implemented as a test detector) (#2014)
Browse files Browse the repository at this point in the history
* CFG Visualizer (implemented as a test detector)

Sometimes, during development it is an advantage if the CFG can be seen visually. This patch adds a detector
that generates the CFG in DOT format so it can be viewed (or converted to a graphical format) using GraphViz.

* Fix for filename generation

* Update according to the comments of @KengoTODA.

* Spotless error fixed.

* CHANGELOG updated

Co-authored-by: Kengo TODA <skypencil@gmail.com>
  • Loading branch information
Balogh, Ádám and KengoTODA committed May 4, 2022
1 parent 85ebe28 commit 9725e34
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 2 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Expand Up @@ -24,9 +24,8 @@ Currently the versioning policy of this project follows [Semantic Versioning v2.
* `THROWS_METHOD_THROWS_CLAUSE_THROWABLE` is reported when a method has Throwable in its throws clause (See [SEI CERT ERR07-J](https://wiki.sei.cmu.edu/confluence/display/java/ERR07-J.+Do+not+throw+RuntimeException%2C+Exception%2C+or+Throwable))
* New rule `PERM_SUPER_NOT_CALLED_IN_GETPERMISSIONS` to warn for custom class loaders who do not call their superclasses' `getPermissions()` in their `getPermissions()` method. This rule based on the SEI CERT rule *SEC07-J Call the superclass's getPermissions() method when writing a custom class loader*. ([#SEC07-J](https://wiki.sei.cmu.edu/confluence/display/java/SEC07-J.+Call+the+superclass%27s+getPermissions%28%29+method+when+writing+a+custom+class+loader))
* New rule `USC_POTENTIAL_SECURITY_CHECK_BASED_ON_UNTRUSTED_SOURCE` to detect cases where a non-final method of a non-final class is called from public methods of public classes and then the same method is called on the same object inside a doPrivileged block. Since the called method may have been overridden to behave differently on the first and second invocations this is a possible security check based on an unreliable source. This rule is based on *SEC02-J. Do not base security checks on untrusted sources*. ([#SEC02-J](https://wiki.sei.cmu.edu/confluence/display/java/SEC02-J.+Do+not+base+security+checks+on+untrusted+sources))

### Added
* New detector `DontUseFloatsAsLoopCounters` to detect usage of floating-point variables as loop counters (`FL_FLOATS_AS_LOOP_COUNTERS`), according to SEI CERT rules [NUM09-J. Do not use floating-point variables as loop counters](https://wiki.sei.cmu.edu/confluence/display/java/NUM09-J.+Do+not+use+floating-point+variables+as+loop+counters)
* New test detector `ViewCFG` to visualize the control-flow graph for `SpotBugs` developers

## 4.6.0 - 2022-03-08
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions spotbugs/etc/findbugs.xml
Expand Up @@ -623,6 +623,8 @@
reports="IO_APPENDING_TO_OBJECT_OUTPUT_STREAM"/>
<Detector class="edu.umd.cs.findbugs.detect.TestASM" speed="fast" disabled="true"
hidden="true" reports="TESTING"/>
<Detector class="edu.umd.cs.findbugs.detect.ViewCFG" disabled="true"
hidden="true"/>
<Detector class="edu.umd.cs.findbugs.detect.FindUnrelatedTypesInGenericContainer"
speed="fast"
reports="GC_UNRELATED_TYPES,DMI_COLLECTIONS_SHOULD_NOT_CONTAIN_THEMSELVES,DMI_USING_REMOVEALL_TO_CLEAR_COLLECTION,DMI_VACUOUS_SELF_COLLECTION_CALL,GC_UNCHECKED_TYPE_IN_GENERIC_CALL"/>
Expand Down
7 changes: 7 additions & 0 deletions spotbugs/etc/messages.xml
Expand Up @@ -1586,6 +1586,13 @@ factory pattern to create these objects.</p>
]]>
</Details>
</Detector>
<Detector class="edu.umd.cs.findbugs.detect.ViewCFG">
<Details>
<![CDATA[
<p> Generate DOT files from the CFGs. </p>
]]>
</Details>
</Detector>
<Detector class="edu.umd.cs.findbugs.detect.FindUnrelatedTypesInGenericContainer">
<Details>
<![CDATA[
Expand Down
191 changes: 191 additions & 0 deletions spotbugs/src/main/java/edu/umd/cs/findbugs/detect/ViewCFG.java
@@ -0,0 +1,191 @@
package edu.umd.cs.findbugs.detect;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.regex.Pattern;

import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.InstructionHandle;

import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.ba.BasicBlock;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.CFGBuilderException;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.Edge;
import static edu.umd.cs.findbugs.ba.EdgeTypes.*;

public class ViewCFG implements Detector {

private final Pattern SPACE_ARROW = Pattern.compile(" ->");
private final Pattern NUMBER_SUFFIX = Pattern.compile(" (\\d+)$");
private final Pattern SPECIAL_METHOD = Pattern.compile("<(\\w+)>");

private final BugReporter bugReporter;
private Path tempDir;

public ViewCFG(BugReporter bugReporter) {
this.bugReporter = bugReporter;
try {
tempDir = Files.createTempDirectory("cfg-");
} catch (IOException e) {
bugReporter.logError("Could not create temporary directory", e);
}
}

@Override
public void visitClassContext(ClassContext classContext) {
if (tempDir == null) {
return;
}

JavaClass cls = classContext.getJavaClass();
String classDirName = (!cls.getPackageName().isEmpty()) ? (cls.getPackageName() + "." + cls.getClassName()) : cls.getClassName();
Path classDir;

try {
classDir = Files.createDirectory(Paths.get(tempDir.toString(), classDirName));
} catch (IOException e) {
bugReporter.logError("Could not create directory for class " + cls.getClassName(), e);
return;
}

for (Method method : cls.getMethods()) {
try {
analyzeMethod(classContext, method, classDir);
} catch (CFGBuilderException e) {
bugReporter.logError("Error analyzing method", e);
}
}
}

private void analyzeMethod(ClassContext classContext, Method method, Path classDir) throws CFGBuilderException {
Path methodFile = getMethodFile(classDir, method.getName());
PrintStream out;

try {
out = new PrintStream(Files.createFile(methodFile).toFile(), Charset.defaultCharset().name());
} catch (IOException e) {
bugReporter.logError("Could not create file for method " + method.getName(), e);
return;
}

CFG cfg = classContext.getCFG(method);
out.println("digraph " + method.getName() + " {");
for (Iterator<BasicBlock> bi = cfg.blockIterator(); bi.hasNext();) {
BasicBlock block = bi.next();
if (block == cfg.getEntry()) {
out.println(" Node" + block.getLabel() + " [shape=record label=\"{" + block.getLabel() +
" (ENTRY) }\"];");
continue;
}

if (block == cfg.getExit()) {
out.println(" Node" + block.getLabel() + " [shape=record label=\"{" + block.getLabel() +
" (EXIT) }\"];");
continue;
}

out.print(" Node" + block.getLabel() + " [shape=record label=\"{" + block.getLabel());
if (block.getFirstInstruction() != null) {
out.print("|");
}
for (Iterator<InstructionHandle> ii = block.instructionIterator(); ii.hasNext();) {
InstructionHandle ins = ii.next();
String insStr = NUMBER_SUFFIX.matcher(
SPACE_ARROW.matcher(ins.toString(false)).replaceAll(""))
.replaceAll(" #$1");
out.print(insStr + "\\l");
}
out.println("}\"];");
}

for (Iterator<Edge> ei = cfg.edgeIterator(); ei.hasNext();) {
Edge edge = ei.next();
BasicBlock src = edge.getSource();
BasicBlock tgt = edge.getTarget();
switch (edge.getType()) {
default:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() + ";");
break;
case IFCMP_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" True branch\"];");
break;
case HANDLED_EXCEPTION_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" Handled exception for #" +
edge.getSource().getExceptionThrower().getPosition() + "\"];");
break;
case UNHANDLED_EXCEPTION_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" Unhandled exception for #" +
edge.getSource().getExceptionThrower().getPosition() + "\"];");
break;
case RETURN_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" Return\"];");
break;
case START_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" Start\"];");
break;
case EXIT_EDGE:
out.println(" Node" + src.getLabel() + " -> Exit" + tgt.getLabel() +
" [shape=plaintext label=\" Exit\"];");
break;
case SWITCH_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" Switch case (non-default)\"];");
break;
case SWITCH_DEFAULT_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" Switch case (default)\"];");
break;
case JSR_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" JSR statement\"];");
break;
case RET_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" RET statement\"];");
break;
case GOTO_EDGE:
out.println(" Node" + src.getLabel() + " -> Node" + tgt.getLabel() +
" [shape=plaintext label=\" GOTO statement\"];");
break;
}
}
out.println("}");
out.close();
if (out.checkError()) {
bugReporter.logError("Error writing to file " + methodFile.toString());
}
}

private Path getMethodFile(Path classDir, String methodName) {
String methodFileName = SPECIAL_METHOD.matcher(methodName).replaceAll("____$1");
Path methodFile;
int index = 0;

do {
methodFile = Paths.get(classDir.toString(), methodFileName + ".dot");
methodFileName = methodName + ++index;
} while (Files.exists(methodFile));
return methodFile;
}

@Override
public void report() {
if (tempDir != null) {
System.out.println("CFGs generated into directory: " + tempDir + ". Please do not forget to delete it.");
}
}
}

0 comments on commit 9725e34

Please sign in to comment.