Skip to content

Java/Cfg: Introduce new shared CFG library and replace the Java CFG.#21290

Merged
aschackmull merged 34 commits intogithub:mainfrom
aschackmull:cfg/new-shared
Mar 2, 2026
Merged

Java/Cfg: Introduce new shared CFG library and replace the Java CFG.#21290
aschackmull merged 34 commits intogithub:mainfrom
aschackmull:cfg/new-shared

Conversation

@aschackmull
Copy link
Contributor

@aschackmull aschackmull commented Feb 6, 2026

This PR introduces a new shared CFG library, which encapsulated the control flow semantics of the most common AST constructs. Additionally, the Java CFG is replaced with an instantiation of this new library.
Control flow semantics of individual AST nodes are given in a modular non-recursive way, which allows seamless integration of the few Java-specific control flow definitions with the shared parts by way of simple union.

Any existing Java libraries with implicit reliance on the exact structure of the CFG have been updated.

Commit-by-commit review is strongly encouraged. There are a few initial setup commits, then the big commit with the main change followed by a long tail of commits to fix various tests and dependent Java libraries.

@aschackmull aschackmull force-pushed the cfg/new-shared branch 6 times, most recently from f54bdf4 to 0e7c3ba Compare February 16, 2026 14:04
@aschackmull aschackmull force-pushed the cfg/new-shared branch 4 times, most recently from 3437f86 to b90ee55 Compare February 18, 2026 09:25
@aschackmull aschackmull force-pushed the cfg/new-shared branch 6 times, most recently from df51520 to 75b9346 Compare February 23, 2026 14:05
@aschackmull aschackmull marked this pull request as ready for review February 23, 2026 14:13
@aschackmull aschackmull requested a review from a team as a code owner February 23, 2026 14:13
Copilot AI review requested due to automatic review settings February 23, 2026 14:13
@aschackmull aschackmull requested review from a team as code owners February 23, 2026 14:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new shared CFG library that encapsulates control flow semantics of common AST constructs and replaces the Java CFG implementation with an instantiation of this library. The change enables modular, non-recursive control flow definitions that integrate Java-specific semantics seamlessly with shared components.

Changes:

  • Introduces a new ConditionKind abstraction to classify different conditional successor types (Boolean, Nullness, Matching, Emptiness)
  • Adds performance optimizations to Guards.qll using pragma[inline_late] and bindingset annotations
  • Replaces the Java CFG implementation with a shared library instantiation
  • Updates numerous test expectations to reflect the new CFG structure with additional synthetic nodes
  • Removes deprecated Completion.qll and updates SSA, dataflow, and guards implementations
  • Adds test utility modules for working with AST-based CFG views

Reviewed changes

Copilot reviewed 140 out of 143 changed files in this pull request and generated no comments.

Show a summary per file
File Description
shared/controlflow/codeql/controlflow/SuccessorType.qll Adds ConditionKind abstraction and getDual() method for conditional successors
shared/controlflow/codeql/controlflow/Guards.qll Adds strictlyDominatesCheck helper with pragma annotations for performance
shared/controlflow/codeql/controlflow/BasicBlock.qll Adds getEnclosingCallable() method as alias for getScope()
java/ql/test/**/*.expected Updates test expectations to reflect new CFG nodes and edges
java/ql/test/**/*.ql Updates test queries to use AST-based CFG successors
java/ql/test/**/FalseSuccessors.* Removes FalseSuccessors test files (no longer needed)
java/ql/lib/semmle/code/java/metrics/MetricCallable.qll Updates switch case analysis to use AST structure instead of CFG
java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll Removes hasDominanceInformation checks and updates CFG node access
java/ql/lib/semmle/code/java/dataflow/TypeFlow.qll Fixes instanceof disjunction analysis for single-predecessor cases
java/ql/lib/semmle/code/java/controlflow/Guards.qll Simplifies switch case edge detection using MatchingSuccessor
java/ql/lib/utils/test/*.qll Adds test utilities for AST-based CFG views
java/ql/lib/change-notes/2026-02-18-cfg.md Documents breaking changes

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at it all in detail.

* An example of this are resource declarations in Java's try-with-resources
* statement.
*/
default AstNode getTryInit(TryStmt try, int index) { none() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest, why not make this a member predicate on TryStmt, which is clearly marked as being optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because most languages don't need it and making it a member means that it can't have a default implementation, so it ends up mandatory and ruins any chances that a language-specific instantiation can provide TryStmt as a simple alias.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I didn't know it couldn't have a default implementation as a member predicate.

Comment on lines +230 to +233
* Gets an integer indicating the control flow order of a case within a switch.
* This is most often the same as the AST order, but can be different in some
* languages if the language allows a default case to appear before other
* cases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this doesn't have to be contiguous. It would be nice to state that here somehow.

Suggested change
* Gets an integer indicating the control flow order of a case within a switch.
* This is most often the same as the AST order, but can be different in some
* languages if the language allows a default case to appear before other
* cases.
* Gets an integer indicating the control flow order of a case within a switch.
* This is most often the same as the AST order, but can be different in some
* languages if the language allows a default case to appear before other
* cases.
*
* The values do not need to be contiguous; only the relative ordering matters.

Comment on lines +354 to +356
* Holds if `n` is in a conditional context of kind `kind`. For example,
* the left-hand side of a short-circuiting `&&` expression is in a
* boolean conditional context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest making it clearer that this predicate is for adding extra cases on top of the ones already included by default. I also noticed this for beginAbruptCompletion, and there are probably other places.

or
result.getIndex() <= getFirstPatternCase(switch).getIndex()
/** Holds if this catch clause catches all exceptions. */
predicate catchAll(Ast::CatchClause catch) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like catchAll and matchAll are out of order. I expected them below additionalNode.

Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work; I'm really happy with the design.

// the `isAfter(exprstmt.getExpr())` to `isAfter(exprstmt)` case is handled by `propagatesValue` above.
)
or
exists(BlockStmt blockstmt |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected ExprStmts and BlockStmts to be handled by default left-to-right evaluation; perhaps by extending getChild inside the shared library where the 0 child of an ExprStmt is the expression and the ith child of a BlockStmt is the ith statement within.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could do that if we make the default left-to-right case take propagatesValue into account. I can try it out to see if it looks cleaner that way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to do this follow-up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this perhaps something that should be put into the shared library?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly. It was mildly useful for making sense of the Java tests across the CFG upheaval. I guess it might be equally useful for C# when I get there, so that seems like a reasonable time to pull it out into shared code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be in the shared library.

// `(x || y) ?? z`, the `||` may short-circuit with a known boolean
// value `t`, but it occurs in a nullness conditional context, which
// means that the `t0` has nullness kind. In these cases we check
// whether there is an implication that allows translatiion from `t`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

translatiion -> translation

n2 = beforeBody
or
n1.isAfterFalse(case.getGuard()) and
n2.isAfterValue(case, any(MatchingSuccessor t | t.getValue() = false))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to do this follow-up.

Comment on lines 10 to 13
/** A basic block that ends in an exit node. */
class ExitBlock extends BasicBlock {
ExitBlock() { this.getLastNode() instanceof ControlFlow::ExitNode }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK 👍

/** Gets the statement this `Node` corresponds to, if any. */
Stmt asStmt() { this = TStmtNode(result) }
/** Provides an implementation of the AST signature for Java. */
module Ast implements AstSig<Location> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case it should probably be moved into some internal module, I doubt we want to expose it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK 👍

@aschackmull aschackmull merged commit e695477 into github:main Mar 2, 2026
143 of 145 checks passed
@aschackmull aschackmull deleted the cfg/new-shared branch March 2, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants