Merge pull request #21268 from aschackmull/java/view-cfg

Java: Add support for "View CFG" in VSCode.
This commit is contained in:
Anders Schack-Mulligen
2026-02-05 09:48:14 +01:00
committed by GitHub
4 changed files with 256 additions and 148 deletions

45
java/ql/lib/printCfg.ql Normal file
View File

@@ -0,0 +1,45 @@
/**
* @name Print CFG
* @description Produces a representation of a file's Control Flow Graph.
* This query is used by the VS Code extension.
* @id java/print-cfg
* @kind graph
* @tags ide-contextual-queries/print-cfg
*/
import java
external string selectedSourceFile();
private predicate selectedSourceFileAlias = selectedSourceFile/0;
external int selectedSourceLine();
private predicate selectedSourceLineAlias = selectedSourceLine/0;
external int selectedSourceColumn();
private predicate selectedSourceColumnAlias = selectedSourceColumn/0;
module ViewCfgQueryInput implements ViewCfgQueryInputSig<File> {
predicate selectedSourceFile = selectedSourceFileAlias/0;
predicate selectedSourceLine = selectedSourceLineAlias/0;
predicate selectedSourceColumn = selectedSourceColumnAlias/0;
predicate cfgScopeSpan(
Callable callable, File file, int startLine, int startColumn, int endLine, int endColumn
) {
file = callable.getFile() and
callable.getLocation().getStartLine() = startLine and
callable.getLocation().getStartColumn() = startColumn and
exists(Location loc |
loc.getEndLine() = endLine and
loc.getEndColumn() = endColumn and
loc = callable.getBody().getLocation()
)
}
}
import ViewCfgQuery<File, ViewCfgQueryInput>

View File

@@ -1775,3 +1775,17 @@ class ConditionNode extends ControlFlow::Node {
/** Gets the condition of this `ConditionNode`. */
ExprParent getCondition() { result = this.asExpr() or result = this.asStmt() }
}
private import codeql.controlflow.PrintGraph as PrintGraph
private module PrintGraphInput implements PrintGraph::InputSig<Location> {
private import java as J
class Callable = J::Callable;
class ControlFlowNode = J::ControlFlowNode;
ControlFlowNode getASuccessor(ControlFlowNode n, SuccessorType t) { result = n.getASuccessor(t) }
}
import PrintGraph::PrintGraph<Location, PrintGraphInput>

View File

@@ -1122,6 +1122,9 @@ module MakeWithSplitting<
/** Gets the scope of this node. */
CfgScope getScope() { result = getNodeCfgScope(this) }
/** Gets the enclosing callable of this node. */
CfgScope getEnclosingCallable() { result = this.getScope() }
/** Gets a successor node of a given type, if any. */
Node getASuccessor(SuccessorType t) { result = getASuccessor(this, t) }
@@ -1310,160 +1313,19 @@ module MakeWithSplitting<
}
}
/** A node to be included in the output of `TestOutput`. */
signature class RelevantNodeSig extends Node;
private import PrintGraph as Pp
/**
* Import this module into a `.ql` file to output a CFG. The
* graph is restricted to nodes from `RelevantNode`.
*/
module TestOutput<RelevantNodeSig RelevantNode> {
/** Holds if `pred -> succ` is an edge in the CFG. */
query predicate edges(RelevantNode pred, RelevantNode succ, string label) {
label =
strictconcat(SuccessorType t, string s |
succ = getASuccessor(pred, t) and
if t instanceof DirectSuccessor then s = "" else s = t.toString()
|
s, ", " order by s
)
}
private module PrintGraphInput implements Pp::InputSig<Location> {
class Callable = CfgScope;
/**
* Provides logic for representing a CFG as a [Mermaid diagram](https://mermaid.js.org/).
*/
module Mermaid {
private string nodeId(RelevantNode n) {
result =
any(int i |
n =
rank[i](RelevantNode p, string filePath, int startLine, int startColumn, int endLine,
int endColumn |
p.getLocation()
.hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn)
|
p order by filePath, startLine, startColumn, endLine, endColumn, p.toString()
)
).toString()
}
class ControlFlowNode = Node;
private string nodes() {
result =
concat(RelevantNode n, string id, string text |
id = nodeId(n) and
text = n.toString()
|
id + "[\"" + text + "\"]", "\n" order by id
)
}
private string edge(RelevantNode pred, RelevantNode succ) {
edges(pred, succ, _) and
exists(string label |
edges(pred, succ, label) and
if label = ""
then result = nodeId(pred) + " --> " + nodeId(succ)
else result = nodeId(pred) + " -- " + label + " --> " + nodeId(succ)
)
}
private string edges() {
result =
concat(RelevantNode pred, RelevantNode succ, string edge, string filePath, int startLine,
int startColumn, int endLine, int endColumn |
edge = edge(pred, succ) and
pred.getLocation().hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn)
|
edge, "\n"
order by
filePath, startLine, startColumn, endLine, endColumn, pred.toString()
)
}
/** Holds if the Mermaid representation is `s`. */
query predicate mermaid(string s) { s = "flowchart TD\n" + nodes() + "\n\n" + edges() }
ControlFlowNode getASuccessor(ControlFlowNode n, SuccessorType t) {
result = n.getASuccessor(t)
}
}
/** Provides the input to `ViewCfgQuery`. */
signature module ViewCfgQueryInputSig<FileSig File> {
/** The source file selected in the IDE. Should be an `external` predicate. */
string selectedSourceFile();
/** The source line selected in the IDE. Should be an `external` predicate. */
int selectedSourceLine();
/** The source column selected in the IDE. Should be an `external` predicate. */
int selectedSourceColumn();
/**
* Holds if CFG scope `scope` spans column `startColumn` of line `startLine` to
* column `endColumn` of line `endLine` in `file`.
*/
predicate cfgScopeSpan(
CfgScope scope, File file, int startLine, int startColumn, int endLine, int endColumn
);
}
/**
* Provides an implementation for a `View CFG` query.
*
* Import this module into a `.ql` that looks like
*
* ```ql
* @name Print CFG
* @description Produces a representation of a file's Control Flow Graph.
* This query is used by the VS Code extension.
* @id <lang>/print-cfg
* @kind graph
* @tags ide-contextual-queries/print-cfg
* ```
*/
module ViewCfgQuery<FileSig File, ViewCfgQueryInputSig<File> ViewCfgQueryInput> {
private import ViewCfgQueryInput
bindingset[file, line, column]
private CfgScope smallestEnclosingScope(File file, int line, int column) {
result =
min(CfgScope scope, int startLine, int startColumn, int endLine, int endColumn |
cfgScopeSpan(scope, file, startLine, startColumn, endLine, endColumn) and
(
startLine < line
or
startLine = line and startColumn <= column
) and
(
endLine > line
or
endLine = line and endColumn >= column
)
|
scope order by startLine desc, startColumn desc, endLine, endColumn
)
}
private import IdeContextual<File>
private class RelevantNode extends Node {
RelevantNode() {
this.getScope() =
smallestEnclosingScope(getFileBySourceArchiveName(selectedSourceFile()),
selectedSourceLine(), selectedSourceColumn())
}
string getOrderDisambiguation() { result = "" }
}
private module Output = TestOutput<RelevantNode>;
import Output::Mermaid
/** Holds if `pred` -> `succ` is an edge in the CFG. */
query predicate edges(RelevantNode pred, RelevantNode succ, string attr, string val) {
attr = "semmle.label" and
Output::edges(pred, succ, val)
}
}
import Pp::PrintGraph<Location, PrintGraphInput>
/** Provides a set of consistency queries. */
module Consistency {

View File

@@ -0,0 +1,187 @@
/**
* Provides modules for printing control flow graphs in VSCode via the "View
* CFG" query. Also provides modules for printing control flow graphs in tests
* and as Mermaid diagrams.
*/
overlay[local?]
module;
private import codeql.util.FileSystem
private import codeql.util.Location
private import SuccessorType
signature module InputSig<LocationSig Location> {
class Callable;
class ControlFlowNode {
Callable getEnclosingCallable();
Location getLocation();
string toString();
}
ControlFlowNode getASuccessor(ControlFlowNode n, SuccessorType t);
}
/** Provides modules for printing control flow graphs. */
module PrintGraph<LocationSig Location, InputSig<Location> Input> {
private import Input
/** A node to be included in the output of `TestOutput`. */
signature class RelevantNodeSig extends ControlFlowNode;
/**
* Import this module into a `.ql` file to output a CFG. The
* graph is restricted to nodes from `RelevantNode`.
*/
module TestOutput<RelevantNodeSig RelevantNode> {
/** Holds if `pred -> succ` is an edge in the CFG. */
query predicate edges(RelevantNode pred, RelevantNode succ, string label) {
label =
strictconcat(SuccessorType t, string s |
succ = getASuccessor(pred, t) and
if t instanceof DirectSuccessor then s = "" else s = t.toString()
|
s, ", " order by s
)
}
/**
* Provides logic for representing a CFG as a [Mermaid diagram](https://mermaid.js.org/).
*/
module Mermaid {
private string nodeId(RelevantNode n) {
result =
any(int i |
n =
rank[i](RelevantNode p, string filePath, int startLine, int startColumn, int endLine,
int endColumn |
p.getLocation()
.hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn)
|
p order by filePath, startLine, startColumn, endLine, endColumn, p.toString()
)
).toString()
}
private string nodes() {
result =
concat(RelevantNode n, string id, string text |
id = nodeId(n) and
text = n.toString()
|
id + "[\"" + text + "\"]", "\n" order by id
)
}
private string edge(RelevantNode pred, RelevantNode succ) {
edges(pred, succ, _) and
exists(string label |
edges(pred, succ, label) and
if label = ""
then result = nodeId(pred) + " --> " + nodeId(succ)
else result = nodeId(pred) + " -- " + label + " --> " + nodeId(succ)
)
}
private string edges() {
result =
concat(RelevantNode pred, RelevantNode succ, string edge, string filePath, int startLine,
int startColumn, int endLine, int endColumn |
edge = edge(pred, succ) and
pred.getLocation().hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn)
|
edge, "\n"
order by
filePath, startLine, startColumn, endLine, endColumn, pred.toString()
)
}
/** Holds if the Mermaid representation is `s`. */
query predicate mermaid(string s) { s = "flowchart TD\n" + nodes() + "\n\n" + edges() }
}
}
/** Provides the input to `ViewCfgQuery`. */
signature module ViewCfgQueryInputSig<FileSig File> {
/** Gets the source file selected in the IDE. Should be an `external` predicate. */
string selectedSourceFile();
/** Gets the source line selected in the IDE. Should be an `external` predicate. */
int selectedSourceLine();
/** Gets the source column selected in the IDE. Should be an `external` predicate. */
int selectedSourceColumn();
/**
* Holds if `callable` spans column `startColumn` of line `startLine` to
* column `endColumn` of line `endLine` in `file`.
*/
predicate cfgScopeSpan(
Callable callable, File file, int startLine, int startColumn, int endLine, int endColumn
);
}
/**
* Provides an implementation for a `View CFG` query.
*
* Import this module into a `.ql` that looks like
*
* ```ql
* @name Print CFG
* @description Produces a representation of a file's Control Flow Graph.
* This query is used by the VS Code extension.
* @id <lang>/print-cfg
* @kind graph
* @tags ide-contextual-queries/print-cfg
* ```
*/
module ViewCfgQuery<FileSig File, ViewCfgQueryInputSig<File> ViewCfgQueryInput> {
private import ViewCfgQueryInput
bindingset[file, line, column]
private Callable smallestEnclosingScope(File file, int line, int column) {
result =
min(Callable callable, int startLine, int startColumn, int endLine, int endColumn |
cfgScopeSpan(callable, file, startLine, startColumn, endLine, endColumn) and
(
startLine < line
or
startLine = line and startColumn <= column
) and
(
endLine > line
or
endLine = line and endColumn >= column
)
|
callable order by startLine desc, startColumn desc, endLine, endColumn
)
}
private import IdeContextual<File>
final private class FinalControlFlowNode = ControlFlowNode;
private class RelevantNode extends FinalControlFlowNode {
RelevantNode() {
this.getEnclosingCallable() =
smallestEnclosingScope(getFileBySourceArchiveName(selectedSourceFile()),
selectedSourceLine(), selectedSourceColumn())
}
string getOrderDisambiguation() { result = "" }
}
private module Output = TestOutput<RelevantNode>;
import Output::Mermaid
/** Holds if `pred` -> `succ` is an edge in the CFG. */
query predicate edges(RelevantNode pred, RelevantNode succ, string attr, string val) {
attr = "semmle.label" and
Output::edges(pred, succ, val)
}
}
}