From aab9cb67288b5182d9138d0a09657b7824fa9fa6 Mon Sep 17 00:00:00 2001 From: Anders Schack-Mulligen Date: Fri, 28 Aug 2026 13:59:30 +0200 Subject: [PATCH 1/2] Unified: Add control flow graph. --- .../ql/consistency-queries/CfgConsistency.ql | 2 + unified/ql/consistency-queries/qlpack.yml | 5 + .../unified/internal/ControlFlowGraph.qll | 277 ++++++++++++++++++ .../lib/codeql/unified/internal/FacadeAst.qll | 8 + .../ql/lib/ide-contextual-queries/printCfg.ql | 40 +++ unified/ql/lib/qlpack.yml | 3 +- unified/ql/lib/unified.qll | 1 + 7 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 unified/ql/consistency-queries/CfgConsistency.ql create mode 100644 unified/ql/consistency-queries/qlpack.yml create mode 100644 unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll create mode 100644 unified/ql/lib/ide-contextual-queries/printCfg.ql diff --git a/unified/ql/consistency-queries/CfgConsistency.ql b/unified/ql/consistency-queries/CfgConsistency.ql new file mode 100644 index 000000000000..5bbfd7e22170 --- /dev/null +++ b/unified/ql/consistency-queries/CfgConsistency.ql @@ -0,0 +1,2 @@ +import unified +import ControlFlow::Consistency diff --git a/unified/ql/consistency-queries/qlpack.yml b/unified/ql/consistency-queries/qlpack.yml new file mode 100644 index 000000000000..3013f12c3257 --- /dev/null +++ b/unified/ql/consistency-queries/qlpack.yml @@ -0,0 +1,5 @@ +name: codeql/unified-consistency-queries +groups: [unified, test, consistency-queries] +dependencies: + codeql/unified-all: ${workspace} +warnOnImplicitThis: true diff --git a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll new file mode 100644 index 000000000000..18f98ad57913 --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll @@ -0,0 +1,277 @@ +/** + * Provides classes representing the control flow graph within callables. + */ +overlay[local?] +module; + +import unified +private import codeql.controlflow.ControlFlowGraph +private import codeql.controlflow.SuccessorType + +private module Cfg0 = Make0; + +private module Cfg1 = Make1; + +private module Cfg2 = Make2; + +private import Cfg0 +private import Cfg1 +private import Cfg2 +import Public + +/** Provides an implementation of the AST signature for Unified. */ +private module Ast implements AstSig { + private import unified as U + + class AstNode = U::AstNode; + + private predicate skipControlFlow(AstNode e) { e instanceof Modifier or e instanceof Identifier } + + AstNode getChild(AstNode n, int index) { + result.getParent() = n and + result.getParentIndex() = index and + not n instanceof Callable and + not skipControlFlow(n) and + not skipControlFlow(result) + } + + Callable getEnclosingCallable(AstNode node) { + exists(AstNode parent | parent = node.getParent() | + result = parent + or + not parent instanceof Callable and + result = getEnclosingCallable(parent) + ) + } + + class Callable = U::Callable; + + AstNode callableGetBody(Callable c) { + result = c.(AccessorDeclaration).getBody() or + result = c.(ConstructorDeclaration).getBody() or + result = c.(DestructorDeclaration).getBody() or + result = c.(FunctionDeclaration).getBody() or + result = c.(FunctionExpr).getBody() or + result = c.(InitializerDeclaration).getBody() or + result = c.(TopLevel).getBody() + } + + class Parameter extends U::Parameter { + Expr getDefaultValue() { result = super.getDefault() } + + AstNode getPattern() { result = super.getPattern() } + } + + Parameter callableGetParameter(Callable c, int index) { + result = c.(AccessorDeclaration).getParameter(index) or + result = c.(ConstructorDeclaration).getParameter(index) or + result = c.(FunctionDeclaration).getParameter(index) or + result = c.(FunctionExpr).getParameter(index) + } + + class Stmt = U::Stmt; + + class Expr = U::Expr; + + class BlockStmt = U::Block; + + class ExprStmt extends Stmt { + ExprStmt() { none() } + + Expr getExpr() { none() } + } + + class IfStmt extends Stmt { + IfStmt() { none() } + + Expr getCondition() { none() } + + Stmt getThen() { none() } + + Stmt getElse() { none() } + } + + abstract class LoopStmt extends Stmt { + Stmt getBody() { none() } + } + + class WhileStmt extends LoopStmt instanceof U::WhileStmt { + override Stmt getBody() { result = U::WhileStmt.super.getBody() } + + Expr getCondition() { result = super.getCondition() } + } + + class DoStmt extends LoopStmt instanceof U::DoWhileStmt { + override Stmt getBody() { result = U::DoWhileStmt.super.getBody() } + + Expr getCondition() { result = super.getCondition() } + } + + class UntilStmt extends LoopStmt { + UntilStmt() { none() } + + Expr getCondition() { none() } + } + + class ForStmt extends LoopStmt { + ForStmt() { none() } + + AstNode getInit(int index) { none() } + + Expr getCondition() { none() } + + AstNode getUpdate(int index) { none() } + } + + class ForeachStmt extends LoopStmt instanceof U::ForEachStmt { + override Stmt getBody() { result = U::ForEachStmt.super.getBody() } + + // TODO support foreach guard + // + // TODO: Expr != Pattern + Expr getVariable() { result = super.getPattern() } + + Expr getCollection() { result = super.getIterable() } + } + + class BreakStmt = U::BreakExpr; + + class ContinueStmt = U::ContinueExpr; + + class GotoStmt extends Stmt { + GotoStmt() { none() } + } + + class ReturnStmt extends U::ReturnExpr { + Expr getExpr() { result = super.getValue() } + } + + class Throw extends U::ThrowExpr { + Expr getExpr() { result = super.getValue() } + } + + class TryStmt extends U::TryExpr { + AstNode getBody(int index) { index = 0 and result = super.getBody() } + + CatchClause getCatch(int index) { result = super.getCatchClause(index) } + + Stmt getFinally() { none() } + } + + class CatchClause extends U::CatchClause { + AstNode getPattern() { result = super.getPattern() } + + AstNode getVariable() { none() } + + Expr getCondition() { none() } + + Stmt getBody() { result = super.getBody() } + } + + class Switch extends U::SwitchExpr { + Expr getExpr() { result = super.getValue() } + + Case getCase(int index) { result = super.getCase(index) } + + Stmt getStmt(int index) { none() } + } + + class Case extends U::SwitchCase { + AstNode getPattern(int index) { result = super.getPattern() and index = 0 } + + Expr getGuard() { none() } + + AstNode getBody() { result = super.getBody() } + } + + class DefaultCase extends Case { + DefaultCase() { not exists(super.getPattern()) } + } + + class ConditionalExpr = U::IfExpr; + + // TODO: sort out the relationship between BinaryExpr and Assignment + class BinaryExpr extends U::BinaryExpr { + Expr getLeftOperand() { result = super.getLeft() } + + Expr getRightOperand() { result = super.getRight() } + } + + class LogicalAndExpr extends BinaryExpr, U::LogicalAndExpr { } + + class LogicalOrExpr extends BinaryExpr, U::LogicalOrExpr { } + + class NullCoalescingExpr extends BinaryExpr, U::NullCoalescingExpr { } + + class UnaryExpr = U::UnaryExpr; + + class LogicalNotExpr = U::LogicalNotExpr; + + // TODO + class Assignment extends BinaryExpr { + Assignment() { none() } + } + + class AssignExpr extends Assignment { } + + class CompoundAssignment extends Assignment { } + + class AssignLogicalAndExpr extends CompoundAssignment { } + + class AssignLogicalOrExpr extends CompoundAssignment { } + + class AssignNullCoalescingExpr extends CompoundAssignment { } + + class BooleanLiteral extends U::BooleanLiteral { + boolean getValue() { result.toString() = super.getValue() } + } + + class PatternMatchExpr extends U::PatternGuardExpr { + Expr getExpr() { result = super.getValue() } + + AstNode getPattern() { result = super.getPattern() } + } +} + +private module Input implements InputSig1, InputSig2 { + private import codeql.util.Void + + predicate cfgCachedStageRef() { CfgCachedStage::ref() } + + class Label extends string { + Label() { + any(LabeledStmt l).getLabel().getValue() = this or + any(BreakExpr b).getLabel().getValue() = this or + any(ContinueExpr c).getLabel().getValue() = this + } + + string toString() { result = this } + } + + private Label getLabelOfStmt(Stmt s) { + exists(LabeledStmt l | s = l.getStmt() | + result = l.getLabel().getValue() or + result = getLabelOfStmt(l) + ) + } + + predicate hasLabel(Ast::AstNode n, Label l) { + l = getLabelOfStmt(n) + or + l = n.(BreakExpr).getLabel().getValue() + or + l = n.(ContinueExpr).getLabel().getValue() + } + + class CallableContext = Void; + + predicate beginAbruptCompletion( + AstNode ast, PreControlFlowNode n, AbruptCompletion c, boolean always + ) { + none() + } + + predicate endAbruptCompletion(AstNode ast, PreControlFlowNode n, AbruptCompletion c) { none() } + + predicate step(PreControlFlowNode n1, PreControlFlowNode n2) { none() } +} diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index af66648a345c..3da8de4b8f03 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -39,6 +39,14 @@ module Unified { } } + /** A block statement. */ + class Block extends G::Block { + /** Gets the last statement in this block. */ + Stmt getLastStmt() { + exists(int i | result = this.getStmt(i) and not exists(this.getStmt(i + 1))) + } + } + /** An expression */ class Expr extends G::Expr { /** Gets the string value of this expression, if it is a known string constant. */ diff --git a/unified/ql/lib/ide-contextual-queries/printCfg.ql b/unified/ql/lib/ide-contextual-queries/printCfg.ql new file mode 100644 index 000000000000..f183c5c63cc9 --- /dev/null +++ b/unified/ql/lib/ide-contextual-queries/printCfg.ql @@ -0,0 +1,40 @@ +/** + * @name Print CFG + * @description Produces a representation of a file's Control Flow Graph. + * This query is used by the VS Code extension. + * @id unified/print-cfg + * @kind graph + * @tags ide-contextual-queries/print-cfg + */ + +private import unified +private import codeql.Locations + +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 ControlFlow::ViewCfgQueryInputSig { + predicate selectedSourceFile = selectedSourceFileAlias/0; + + predicate selectedSourceLine = selectedSourceLineAlias/0; + + predicate selectedSourceColumn = selectedSourceColumnAlias/0; + + predicate cfgScopeSpan( + Callable scope, File file, int startLine, int startColumn, int endLine, int endColumn + ) { + file = scope.getFile() and + scope.getLocation().hasLocationInfo(_, startLine, startColumn, endLine, endColumn) + } +} + +import ControlFlow::ViewCfgQuery diff --git a/unified/ql/lib/qlpack.yml b/unified/ql/lib/qlpack.yml index d67af16271b6..0167b114ba96 100644 --- a/unified/ql/lib/qlpack.yml +++ b/unified/ql/lib/qlpack.yml @@ -6,7 +6,8 @@ extractor: unified library: true upgrades: upgrades dependencies: - codeql/util: ${workspace} + codeql/controlflow: ${workspace} codeql/namebinding: ${workspace} + codeql/util: ${workspace} warnOnImplicitThis: true compileForOverlayEval: true diff --git a/unified/ql/lib/unified.qll b/unified/ql/lib/unified.qll index 67d6ce7558ea..033d00aa6def 100644 --- a/unified/ql/lib/unified.qll +++ b/unified/ql/lib/unified.qll @@ -6,4 +6,5 @@ import codeql.Locations import codeql.files.FileSystem import codeql.unified.internal.Ast::UnifiedFinal import codeql.unified.internal.AstExtra::Public +import codeql.unified.internal.ControlFlowGraph import codeql.unified.internal.LocalNameBinding::Public From f452bd48ebf0cf989ead1f81d769fba38b2f9a9d Mon Sep 17 00:00:00 2001 From: Anders Schack-Mulligen Date: Wed, 2 Sep 2026 10:19:37 +0200 Subject: [PATCH 2/2] Apply suggestion from @asgerf Co-authored-by: Asger F --- unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll index 18f98ad57913..ee27d4a28bc8 100644 --- a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll @@ -4,7 +4,7 @@ overlay[local?] module; -import unified +private import unified private import codeql.controlflow.ControlFlowGraph private import codeql.controlflow.SuccessorType