From 9725e34f33cf76968422c36b0611ec08b9ced98f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Balogh=2C=20=C3=81d=C3=A1m?=
Date: Wed, 4 May 2022 03:11:58 +0200
Subject: [PATCH] CFG Visualizer (implemented as a test detector) (#2014)
* 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
---
CHANGELOG.md | 3 +-
spotbugs/etc/findbugs.xml | 2 +
spotbugs/etc/messages.xml | 7 +
.../edu/umd/cs/findbugs/detect/ViewCFG.java | 191 ++++++++++++++++++
4 files changed, 201 insertions(+), 2 deletions(-)
create mode 100644 spotbugs/src/main/java/edu/umd/cs/findbugs/detect/ViewCFG.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c923418343..1d5cab2e1fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/spotbugs/etc/findbugs.xml b/spotbugs/etc/findbugs.xml
index 2f30400ddae..8d60497ae13 100644
--- a/spotbugs/etc/findbugs.xml
+++ b/spotbugs/etc/findbugs.xml
@@ -623,6 +623,8 @@
reports="IO_APPENDING_TO_OBJECT_OUTPUT_STREAM"/>
+
diff --git a/spotbugs/etc/messages.xml b/spotbugs/etc/messages.xml
index 02d62e3ada2..249492647ba 100644
--- a/spotbugs/etc/messages.xml
+++ b/spotbugs/etc/messages.xml
@@ -1586,6 +1586,13 @@ factory pattern to create these objects.
]]>
+
+
+ Generate DOT files from the CFGs.
+ ]]>
+
+
");
+ 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 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 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 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.");
+ }
+ }
+}