Merge pull request #21743 from hvitved/cfg/body-parts

C#: Move handling of callables into shared control flow library
This commit is contained in:
Tom Hvitved
2026-04-23 14:10:46 +02:00
committed by GitHub
3 changed files with 147 additions and 121 deletions

View File

@@ -25,7 +25,7 @@ private module Ast implements AstSig<Location> {
class AstNode = ControlFlowElementOrCallable;
private predicate skipControlFlow(AstNode e) {
additional predicate skipControlFlow(AstNode e) {
e instanceof TypeAccess and
not e instanceof TypeAccessPatternExpr
or
@@ -82,13 +82,7 @@ private module Ast implements AstSig<Location> {
AstNode callableGetBody(Callable c) {
not skipControlFlow(result) and
(
result = c.getBody() or
result = c.(Constructor).getObjectInitializerCall() or
result = c.(Constructor).getInitializer() or
c.(ObjectInitMethod).initializes(result) or
Initializers::staticMemberInitializer(c, result)
)
result = c.getBody()
}
class Stmt = CS::Stmt;
@@ -222,10 +216,21 @@ private module Ast implements AstSig<Location> {
* Unlike the standard `Compilation` class, this class also supports buildless
* extraction.
*/
private newtype CompilationExt =
private newtype TCompilationExt =
TCompilation(Compilation c) { not extractionIsStandalone() } or
TBuildless() { extractionIsStandalone() }
private class CompilationExt extends TCompilationExt {
string toString() {
exists(Compilation c |
this = TCompilation(c) and
result = c.toString()
)
or
this = TBuildless() and result = "buildless compilation"
}
}
/** Gets the compilation that source file `f` belongs to. */
private CompilationExt getCompilation(File f) {
exists(Compilation c |
@@ -286,32 +291,18 @@ private module Initializers {
}
/**
* Gets the `i`th member initializer expression for object initializer method `obinit`
* in compilation `comp`.
* Gets the `i`th member initializer expression for object initializer method `obinit`.
*/
AssignExpr initializedInstanceMemberOrder(ObjectInitMethod obinit, CompilationExt comp, int i) {
obinit.initializes(result) and
AssignExpr initializedInstanceMemberOrder(ObjectInitMethod obinit, int i) {
result =
rank[i + 1](AssignExpr ae0, Location l, string filepath, int startline, int startcolumn |
obinit.initializes(ae0) and
l = ae0.getLocation() and
l.hasLocationInfo(filepath, startline, startcolumn, _, _) and
getCompilation(l.getFile()) = comp
l.hasLocationInfo(filepath, startline, startcolumn, _, _)
|
ae0 order by startline, startcolumn, filepath
)
}
/**
* Gets the last member initializer expression for object initializer method `obinit`
* in compilation `comp`.
*/
AssignExpr lastInitializer(ObjectInitMethod obinit, CompilationExt comp) {
exists(int i |
result = initializedInstanceMemberOrder(obinit, comp, i) and
not exists(initializedInstanceMemberOrder(obinit, comp, i + 1))
)
}
}
private module Exceptions {
@@ -424,6 +415,31 @@ private module Input implements InputSig1, InputSig2 {
l = TLblGoto(n.(LabelStmt).getLabel())
}
class CallableBodyPartContext = CompilationExt;
pragma[nomagic]
Ast::AstNode callableGetBodyPart(Callable c, CallableBodyPartContext ctx, int index) {
not Ast::skipControlFlow(result) and
ctx = getCompilation(result.getFile()) and
(
result = Initializers::initializedInstanceMemberOrder(c, index)
or
result = Initializers::initializedStaticMemberOrder(c, index)
or
exists(Constructor ctor, int i, int staticMembers |
c = ctor and
staticMembers = count(Expr init | Initializers::staticMemberInitializer(ctor, init)) and
index = staticMembers + i + 1
|
i = 0 and result = ctor.getObjectInitializerCall()
or
i = 1 and result = ctor.getInitializer()
or
i = 2 and result = ctor.getBody()
)
)
}
private Expr getQualifier(QualifiableExpr qe) {
result = qe.getQualifier() or
result = qe.(ExtensionMethodCall).getArgument(0)
@@ -474,80 +490,7 @@ private module Input implements InputSig1, InputSig2 {
)
}
pragma[noinline]
private MethodCall getObjectInitializerCall(Constructor ctor, CompilationExt comp) {
result = ctor.getObjectInitializerCall() and
comp = getCompilation(result.getFile())
}
pragma[noinline]
private ConstructorInitializer getInitializer(Constructor ctor, CompilationExt comp) {
result = ctor.getInitializer() and
comp = getCompilation(result.getFile())
}
pragma[noinline]
private Ast::AstNode getBody(Constructor ctor, CompilationExt comp) {
result = ctor.getBody() and
comp = getCompilation(result.getFile())
}
predicate step(PreControlFlowNode n1, PreControlFlowNode n2) {
exists(Constructor ctor |
n1.(EntryNodeImpl).getEnclosingCallable() = ctor and
if Initializers::staticMemberInitializer(ctor, _)
then n2.isBefore(Initializers::initializedStaticMemberOrder(ctor, 0))
else
if exists(ctor.getObjectInitializerCall())
then n2.isBefore(ctor.getObjectInitializerCall())
else
if exists(ctor.getInitializer())
then n2.isBefore(ctor.getInitializer())
else n2.isBefore(ctor.getBody())
or
exists(int i | n1.isAfter(Initializers::initializedStaticMemberOrder(ctor, i)) |
n2.isBefore(Initializers::initializedStaticMemberOrder(ctor, i + 1))
or
not exists(Initializers::initializedStaticMemberOrder(ctor, i + 1)) and
n2.isBefore(ctor.getBody())
)
or
exists(CompilationExt comp |
n1.isAfter(getObjectInitializerCall(ctor, comp)) and
if exists(getInitializer(ctor, comp))
then n2.isBefore(getInitializer(ctor, comp))
else
// This is only relevant in the context of compilation errors, since
// normally the existence of an object initializer call implies the
// existence of an initializer.
if exists(getBody(ctor, comp))
then n2.isBefore(getBody(ctor, comp))
else n2.(NormalExitNodeImpl).getEnclosingCallable() = ctor
or
n1.isAfter(getInitializer(ctor, comp)) and
if exists(getBody(ctor, comp))
then n2.isBefore(getBody(ctor, comp))
else n2.(NormalExitNodeImpl).getEnclosingCallable() = ctor
)
or
n1.isAfter(ctor.getBody()) and
n2.(NormalExitNodeImpl).getEnclosingCallable() = ctor
)
or
exists(ObjectInitMethod obinit |
n1.(EntryNodeImpl).getEnclosingCallable() = obinit and
n2.isBefore(Initializers::initializedInstanceMemberOrder(obinit, _, 0))
or
exists(CompilationExt comp, int i |
// Flow from one member initializer to the next
n1.isAfter(Initializers::initializedInstanceMemberOrder(obinit, comp, i)) and
n2.isBefore(Initializers::initializedInstanceMemberOrder(obinit, comp, i + 1))
)
or
n1.isAfter(Initializers::lastInitializer(obinit, _)) and
n2.(NormalExitNodeImpl).getEnclosingCallable() = obinit
)
or
exists(QualifiableExpr qe | qe.isConditional() |
n1.isBefore(qe) and n2.isBefore(getQualifier(qe))
or

View File

@@ -468,6 +468,7 @@ private module NonReturningCalls {
private module Input implements InputSig1, InputSig2 {
private import java as J
private import codeql.util.Void
predicate cfgCachedStageRef() { CfgCachedStage::ref() }
@@ -533,6 +534,8 @@ private module Input implements InputSig1, InputSig2 {
l = TYield() and n instanceof SwitchExpr
}
class CallableBodyPartContext = Void;
predicate inConditionalContext(Ast::AstNode n, ConditionKind kind) {
kind.isBoolean() and
(

View File

@@ -38,16 +38,16 @@ signature module AstSig<LocationSig Location> {
Location getLocation();
}
/** Gets the child of this AST node at the specified index. */
/** Gets the child of AST node `n` at the specified index. */
AstNode getChild(AstNode n, int index);
/** Gets the immediately enclosing callable that contains this node. */
/** Gets the immediately enclosing callable that contains `node`. */
Callable getEnclosingCallable(AstNode node);
/** A callable, for example a function, method, constructor, or top-level script. */
class Callable extends AstNode;
/** Gets the body of this callable, if any. */
/** Gets the body of callable `c`, if any. */
AstNode callableGetBody(Callable c);
/** A statement. */
@@ -454,6 +454,28 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
default predicate successorValueImplies(ConditionalSuccessor t1, ConditionalSuccessor t2) {
none()
}
/**
* An additional context needed to identify the body parts of a callable.
*
* When not used, instantiate with the `Void` type.
*/
class CallableBodyPartContext {
/** Gets a textual representation of this context. */
string toString();
}
/**
* Gets the `index`th part of the body of `c` in context `ctx`. The indices do not
* need to be consecutive nor start from a specific index.
*
* This overrides the default CFG for a `Callable` with sequential evaluation
* of the body parts, in case a singleton `callableGetBody(c)` is inadequate
* to describe the child nodes of `c`.
*/
default AstNode callableGetBodyPart(Callable c, CallableBodyPartContext ctx, int index) {
none()
}
}
/**
@@ -461,6 +483,8 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
* by subsequent instatiation of `Make2`.
*/
module Make1<InputSig1 Input1> {
private import codeql.util.DenseRank
/**
* Holds if `n` is executed in post-order or in-order. This means that an
* additional node is created to represent `n` in the control flow graph.
@@ -661,6 +685,41 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
* not step to it, since "after" represents normal termination).
*/
private predicate callableHasBodyPart(Callable c, AstNode n) {
n = callableGetBody(c) or n = Input1::callableGetBodyPart(c, _, _)
}
private module BodyPartDenseRankInput implements DenseRankInputSig2 {
class C1 = Callable;
class C2 = Input1::CallableBodyPartContext;
class Ranked = AstNode;
int getRank(C1 c, C2 ctx, Ranked child) {
child = Input1::callableGetBodyPart(c, ctx, result)
}
}
private predicate getRankedBodyPart = DenseRank2<BodyPartDenseRankInput>::denseRank/3;
private AstNode getBodyEntry(Callable c) {
result = callableGetBody(c) and
not exists(getRankedBodyPart(c, _, _))
or
result = getRankedBodyPart(c, _, 1)
}
private AstNode getBodyExit(Callable c) {
result = callableGetBody(c) and
not exists(getRankedBodyPart(c, _, _))
or
exists(Input1::CallableBodyPartContext ctx, int last |
result = getRankedBodyPart(c, ctx, last) and
not exists(getRankedBodyPart(c, ctx, last + 1))
)
}
cached
private newtype TNode =
TBeforeNode(AstNode n) { Input1::cfgCachedStageRef() and exists(getEnclosingCallable(n)) } or
@@ -677,9 +736,9 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
TAdditionalNode(AstNode n, string tag) {
additionalNode(n, tag, _) and exists(getEnclosingCallable(n))
} or
TEntryNode(Callable c) { exists(callableGetBody(c)) } or
TAnnotatedExitNode(Callable c, Boolean normal) { exists(callableGetBody(c)) } or
TExitNode(Callable c) { exists(callableGetBody(c)) }
TEntryNode(Callable c) { callableHasBodyPart(c, _) } or
TAnnotatedExitNode(Callable c, Boolean normal) { callableHasBodyPart(c, _) } or
TExitNode(Callable c) { callableHasBodyPart(c, _) }
private class NodeImpl extends TNode {
/**
@@ -895,7 +954,7 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
}
/** The `PreControlFlowNode` at the entry point of a callable. */
final class EntryNodeImpl extends NodeImpl, TEntryNode {
final private class EntryNodeImpl extends NodeImpl, TEntryNode {
private Callable c;
EntryNodeImpl() { this = TEntryNode(c) }
@@ -1097,7 +1156,7 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
private predicate endAbruptCompletion(AstNode ast, PreControlFlowNode n, AbruptCompletion c) {
Input2::endAbruptCompletion(ast, n, c)
or
exists(Callable callable | ast = callableGetBody(callable) |
exists(Callable callable | callableHasBodyPart(callable, ast) |
c.getSuccessorType() instanceof ReturnSuccessor and
n.(NormalExitNodeImpl).getEnclosingCallable() = callable
or
@@ -1195,10 +1254,16 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
)
}
private Case getRankedCaseCfgOrder(Switch s, int rnk) {
result = rank[rnk](Case c, int i | getCaseControlFlowOrder(s, c) = i | c order by i)
private module CaseDenseRankInput implements DenseRankInputSig1 {
class C = Switch;
class Ranked = Case;
predicate getRank = getCaseControlFlowOrder/2;
}
private predicate getRankedCaseCfgOrder = DenseRank1<CaseDenseRankInput>::denseRank/2;
private int numberOfStmts(Switch s) { result = strictcount(s.getStmt(_)) }
private predicate caseIndex(Switch s, Case c, int caseIdx, int caseStmtPos) {
@@ -1255,22 +1320,20 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
)
}
private predicate hasSpecificCallableSteps(Callable c) {
exists(EntryNodeImpl entry | entry.getEnclosingCallable() = c and Input2::step(entry, _))
}
/** Holds if there is a local non-abrupt step from `n1` to `n2`. */
private predicate explicitStep(PreControlFlowNode n1, PreControlFlowNode n2) {
Input2::step(n1, n2)
or
exists(Callable c |
// Allow language-specific overrides for the default entry and exit edges.
not hasSpecificCallableSteps(c) and
n1.(EntryNodeImpl).getEnclosingCallable() = c and
n2.isBefore(callableGetBody(c))
n2.isBefore(getBodyEntry(c))
or
not hasSpecificCallableSteps(c) and
n1.isAfter(callableGetBody(c)) and
exists(Input1::CallableBodyPartContext ctx, int i |
n1.isAfter(getRankedBodyPart(c, ctx, i)) and
n2.isBefore(getRankedBodyPart(c, ctx, i + 1))
)
or
n1.isAfter(getBodyExit(c)) and
n2.(NormalExitNodeImpl).getEnclosingCallable() = c
or
n1.(AnnotatedExitNodeImpl).getEnclosingCallable() = c and
@@ -1635,11 +1698,19 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
not explicitStep(any(PreControlFlowNode n | n.isBefore(ast)), _)
}
private AstNode getRankedChild(AstNode parent, int rnk) {
defaultCfg(parent) and
result = rank[rnk](AstNode c, int ix | c = getChild(parent, ix) | c order by ix)
private module ChildDenseRankInput implements DenseRankInputSig1 {
class C = AstNode;
class Ranked = AstNode;
int getRank(C parent, Ranked child) {
defaultCfg(parent) and
child = getChild(parent, result)
}
}
private predicate getRankedChild = DenseRank1<ChildDenseRankInput>::denseRank/2;
/**
* Holds if `n1` to `n2` is a default left-to-right evaluation step for
* an `AstNode` that does not otherwise have explicitly defined control
@@ -2128,6 +2199,15 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
query predicate selfLoop(ControlFlowNode node, SuccessorType t) {
node.getASuccessor(t) = node
}
/**
* Holds if `c` does not include `callableGetBody` in a non-empty `callableGetBodyPart`.
*/
query predicate bodyPartOverlap(Callable c) {
exists(callableGetBody(c)) and
exists(Input1::callableGetBodyPart(c, _, _)) and
not Input1::callableGetBodyPart(c, _, _) = callableGetBody(c)
}
}
}
}