Repository: github/codeql
Pull request #19093: Java: Adjust caching of BasicBlocks, BaseSSA, and CompileTimeConstants
Author: login: aschackmull
Labels: name: Java
name: no-change-note-required
description: This PR does not need a change note
Created: 2025-03-21T14:54:36Z | Merged: 2025-03-31T08:48:13Z

This PR makes 3 changes to caching in the Java QL libs (separated cleanly in individual commits):
* Merge the cached stages related to BasicBlocks, and cache the char-pred.
* Merge the cached stages related to BaseSSA, and cache the SourceVariable newtype.
* Values of CompileTimeConstants of type int, bool, and string involve mutually recursive calculation. The int values were cached but the other two weren't, so I fixed that. Since they're mutually recursive they all end up in the same stage.

All 3 of these changes were motivated by statically inspecting DIL overlap between stages. These cases stood out as potentially being able to cause recomputation.

The changes caused a bit of performance fallout, which was fixed in an additional commit.


Diff:
diff --git a/java/ql/lib/semmle/code/java/Expr.qll b/java/ql/lib/semmle/code/java/Expr.qll
index cb02791e96cc..e7dd817cecd9 100644
--- a/java/ql/lib/semmle/code/java/Expr.qll
+++ b/java/ql/lib/semmle/code/java/Expr.qll
@@ -180,7 +180,7 @@ class CompileTimeConstantExpr extends Expr {
   /**
    * Gets the string value of this expression, where possible.
    */
-  pragma[nomagic]
+  cached
   string getStringValue() {
     result = this.(StringLiteral).getValue()
     or
@@ -205,7 +205,7 @@ class CompileTimeConstantExpr extends Expr {
   /**
    * Gets the boolean value of this expression, where possible.
    */
-  pragma[nomagic]
+  cached
   boolean getBooleanValue() {
     // Literal value.
     result = this.(BooleanLiteral).getBooleanValue()
diff --git a/java/ql/lib/semmle/code/java/controlflow/BasicBlocks.qll b/java/ql/lib/semmle/code/java/controlflow/BasicBlocks.qll
index 972f97ba3674..c2f9e8a6a697 100644
--- a/java/ql/lib/semmle/code/java/controlflow/BasicBlocks.qll
+++ b/java/ql/lib/semmle/code/java/controlflow/BasicBlocks.qll
@@ -5,6 +5,19 @@
 import java
 import Dominance
 
+cached
+private module BasicBlockStage {
+  cached
+  predicate ref() { any() }
+
+  cached
+  predicate backref() {
+    (exists(any(BasicBlock bb).getABBSuccessor()) implies any()) and
+    (exists(any(BasicBlock bb).getNode(_)) implies any()) and
+    (exists(any(BasicBlock bb).length()) implies any())
+  }
+}
+
 /**
  * A control-flow node that represents the start of a basic block.
  *
@@ -12,8 +25,11 @@ import Dominance
  * often be treated as a unit in analyses.
  */
 class BasicBlock extends ControlFlowNode {
+  cached
   BasicBlock() {
-    not exists(this.getAPredecessor()) and exists(this.getASuccessor())
+    BasicBlockStage::ref() and
+    not exists(this.getAPredecessor()) and
+    exists(this.getASuccessor())
     or
     strictcount(this.getAPredecessor()) > 1
     or
@@ -24,7 +40,10 @@ class BasicBlock extends ControlFlowNode {
 
   /** Gets an immediate successor of this basic block. */
   cached
-  BasicBlock getABBSuccessor() { result = this.getLastNode().getASuccessor() }
+  BasicBlock getABBSuccessor() {
+    BasicBlockStage::ref() and
+    result = this.getLastNode().getASuccessor()
+  }
 
   /** Gets an immediate predecessor of this basic block. */
   BasicBlock getABBPredecessor() { result.getABBSuccessor() = this }
@@ -35,7 +54,9 @@ class BasicBlock extends ControlFlowNode {
   /** Gets the control-flow node at a specific (zero-indexed) position in this basic block. */
   cached
   ControlFlowNode getNode(int pos) {
-    result = this and pos = 0
+    BasicBlockStage::ref() and
+    result = this and
+    pos = 0
     or
     exists(ControlFlowNode mid, int mid_pos | pos = mid_pos + 1 |
       this.getNode(mid_pos) = mid and
@@ -52,7 +73,10 @@ class BasicBlock extends ControlFlowNode {
 
   /** Gets the number of control-flow nodes contained in this basic block. */
   cached
-  int length() { result = strictcount(this.getANode()) }
+  int length() {
+    BasicBlockStage::ref() and
+    result = strictcount(this.getANode())
+  }
 
   /** Holds if this basic block strictly dominates `node`. */
   predicate bbStrictlyDominates(BasicBlock node) { bbStrictlyDominates(this, node) }
diff --git a/java/ql/lib/semmle/code/java/dataflow/internal/BaseSSA.qll b/java/ql/lib/semmle/code/java/dataflow/internal/BaseSSA.qll
index 1b912f919756..eeac19e66a74 100644
--- a/java/ql/lib/semmle/code/java/dataflow/internal/BaseSSA.qll
+++ b/java/ql/lib/semmle/code/java/dataflow/internal/BaseSSA.qll
@@ -14,9 +14,26 @@
 import java
 private import codeql.ssa.Ssa as SsaImplCommon
 
+cached
+private module BaseSsaStage {
+  cached
+  predicate ref() { any() }
+
+  cached
+  predicate backref() {
+    (exists(TLocalVar(_, _)) implies any()) and
+    (exists(any(BaseSsaSourceVariable v).getAnAccess()) implies any()) and
+    (exists(getAUse(_)) implies any())
+  }
+}
+
+cached
 private newtype TBaseSsaSourceVariable =
   TLocalVar(Callable c, LocalScopeVariable v) {
-    c = v.getCallable() or c = v.getAnAccess().getEnclosingCallable()
+    BaseSsaStage::ref() and
+    c = v.getCallable()
+    or
+    c = v.getAnAccess().getEnclosingCallable()
   }
 
 /**
@@ -31,6 +48,7 @@ class BaseSsaSourceVariable extends TBaseSsaSourceVariable {
    */
   cached
   VarAccess getAnAccess() {
+    BaseSsaStage::ref() and
     exists(LocalScopeVariable v, Callable c |
       this = TLocalVar(c, v) and result = v.getAnAccess() and result.getEnclosingCallable() = c
     )
@@ -188,6 +206,7 @@ cached
 private module Cached {
   cached
   VarRead getAUse(Impl::Definition def) {
+    BaseSsaStage::ref() and
     exists(BaseSsaSourceVariable v, BasicBlock bb, int i |
       Impl::ssaDefReachesRead(v, def, bb, i) and
       result.getControlFlowNode() = bb.getNode(i) and
diff --git a/java/ql/lib/semmle/code/java/environment/SystemProperty.qll b/java/ql/lib/semmle/code/java/environment/SystemProperty.qll
index 63ded626c115..bee91d7c6b7f 100644
--- a/java/ql/lib/semmle/code/java/environment/SystemProperty.qll
+++ b/java/ql/lib/semmle/code/java/environment/SystemProperty.qll
@@ -269,18 +269,24 @@ private MethodCall getSystemPropertyFromSpringProperties(string propertyName) {
  * for final variables.
  */
 private predicate localExprFlowPlusInitializers(Expr e1, Expr e2) {
+  e1 = e2 or
   localFlowPlusInitializers(DataFlow::exprNode(e1), DataFlow::exprNode(e2))
 }
 
+private predicate localFlowPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) =
+  fastTC(localFlowStepPlusInitializers/2)(pred, succ)
+
 /**
- * Holds if data can flow from `pred` to `succ` in zero or more
- * local (intra-procedural) steps or via instance or static variable intializers
+ * Holds if data can flow from `pred` to `succ` in a
+ * local (intra-procedural) step or via instance or static variable intializers
  * for final variables.
  */
-private predicate localFlowPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) {
-  exists(Variable v | v.isFinal() and pred.asExpr() = v.getInitializer() |
-    DataFlow::localFlow(DataFlow::exprNode(v.getAnAccess()), succ)
+private predicate localFlowStepPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) {
+  exists(Variable v |
+    v.isFinal() and
+    pred.asExpr() = v.getInitializer() and
+    succ.asExpr() = v.getAnAccess()
   )
   or
-  DataFlow::localFlow(pred, succ)
+  DataFlow::localFlowStep(pred, succ)
 }


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T14:59:48Z
user: login: hvitved
body: One question, the caching changes LGTM.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T07:39:27Z
user: login: aschackmull
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:26:26Z
user: login: hvitved
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:35:27Z

Review comments:
diff_hunk: @@ -24,7 +40,10 @@
 
   /** Gets an immediate successor of this basic block. */
   cached
-  BasicBlock getABBSuccessor() { result = this.getLastNode().getASuccessor() }
+  BasicBlock getABBSuccessor() {
path: java/ql/lib/semmle/code/java/controlflow/BasicBlocks.qll
user: login: github-advanced-security[bot]
body: ## Acronyms should be PascalCase/camelCase.

Acronyms in getABBSuccessor should be PascalCase/camelCase.

[Show more details](https://github.com/github/codeql/security/code-scanning/94849)
created_at: 2025-03-21T14:59:48Z
author_association: NONE
diff_hunk: @@ -269,18 +269,24 @@ private MethodCall getSystemPropertyFromSpringProperties(string propertyName) {
  * for final variables.
  */
 private predicate localExprFlowPlusInitializers(Expr e1, Expr e2) {
+  e1 = e2 or
   localFlowPlusInitializers(DataFlow::exprNode(e1), DataFlow::exprNode(e2))
 }
 
+private predicate localFlowPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) =
+  fastTC(localFlowStepPlusInitializers/2)(pred, succ)
+
 /**
- * Holds if data can flow from `pred` to `succ` in zero or more
- * local (intra-procedural) steps or via instance or static variable intializers
+ * Holds if data can flow from `pred` to `succ` in a
+ * local (intra-procedural) step or via instance or static variable intializers
  * for final variables.
  */
-private predicate localFlowPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) {
-  exists(Variable v | v.isFinal() and pred.asExpr() = v.getInitializer() |
-    DataFlow::localFlow(DataFlow::exprNode(v.getAnAccess()), succ)
+private predicate localFlowStepPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) {
path: java/ql/lib/semmle/code/java/environment/SystemProperty.qll
user: login: hvitved
body: Is this rewrite semantics preserving? It looks like before it would calculate flow as
```
pred --initializer--> access --local--*> succ
```
and now it is
```
pred --initializer/local--*> succ
```
created_at: 2025-03-31T07:39:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -269,18 +269,24 @@ private MethodCall getSystemPropertyFromSpringProperties(string propertyName) {
  * for final variables.
  */
 private predicate localExprFlowPlusInitializers(Expr e1, Expr e2) {
+  e1 = e2 or
   localFlowPlusInitializers(DataFlow::exprNode(e1), DataFlow::exprNode(e2))
 }
 
+private predicate localFlowPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) =
+  fastTC(localFlowStepPlusInitializers/2)(pred, succ)
+
 /**
- * Holds if data can flow from `pred` to `succ` in zero or more
- * local (intra-procedural) steps or via instance or static variable intializers
+ * Holds if data can flow from `pred` to `succ` in a
+ * local (intra-procedural) step or via instance or static variable intializers
  * for final variables.
  */
-private predicate localFlowPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) {
-  exists(Variable v | v.isFinal() and pred.asExpr() = v.getInitializer() |
-    DataFlow::localFlow(DataFlow::exprNode(v.getAnAccess()), succ)
+private predicate localFlowStepPlusInitializers(DataFlow::Node pred, DataFlow::Node succ) {
path: java/ql/lib/semmle/code/java/environment/SystemProperty.qll
user: login: aschackmull
body: Correct, it's not 100% semantics preserving, but I think the tweaked version makes more sense: If we're doing local flow plus initialization of final fields, then it seems like a mistake to rule out local flow prior to the initialization, like e.g. a cast or something like that.
created_at: 2025-03-31T08:26:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: aschackmull
created_at: 2025-03-25T08:17:06Z
body: Dca looks like a decent performance improvement.
author_association: CONTRIBUTOR
user: login: hvitved
created_at: 2025-03-31T07:28:41Z
body: > Dca looks like a decent performance improvement.

Also a decent [DIL size improvement](https://github.com/github/codeql-dca-main/blob/data/aschackmull/PR-19093-0-java__1/reports/summaries/output.theme.md#dil-sizes).
author_association: CONTRIBUTOR