stage next

This commit is contained in:
2026-06-27 20:03:48 +02:00
parent a7c3b08164
commit 1655f5285c
2 changed files with 457 additions and 3 deletions

View File

@@ -7,6 +7,7 @@ public class JdtDataFlowModel implements DataFlowModel {
private final CodebaseContext context; private final CodebaseContext context;
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>(); private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>(); private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
public JdtDataFlowModel(CodebaseContext context) { public JdtDataFlowModel(CodebaseContext context) {
this.context = context; this.context = context;
@@ -112,6 +113,13 @@ public class JdtDataFlowModel implements DataFlowModel {
} }
} }
// Handle ClassInstanceCreation
if (expr instanceof ClassInstanceCreation cic) {
objectFieldBindings.computeIfAbsent(cic, k -> buildFieldBindingsFromCic(cic, paramBindings, instanceFieldBindings));
visited.remove(expr);
return List.of(cic);
}
// Handle CastExpression // Handle CastExpression
if (expr instanceof CastExpression ce) { if (expr instanceof CastExpression ce) {
List<Expression> resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); List<Expression> resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
@@ -148,11 +156,30 @@ public class JdtDataFlowModel implements DataFlowModel {
} }
} }
// Evaluate mutator/setter method body
evaluateMethodMutations(md, mi.arguments(), target.fieldBindings);
List<ReturnStatement> returns = findReturnStatements(md.getBody()); List<ReturnStatement> returns = findReturnStatements(md.getBody());
if (!returns.isEmpty()) { if (!returns.isEmpty()) {
for (ReturnStatement rs : returns) { for (ReturnStatement rs : returns) {
if (rs.getExpression() != null) { if (rs.getExpression() != null) {
results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, target.fieldBindings, depth + 1)); Expression retExpr = rs.getExpression();
Expression unwrapped = retExpr;
while (unwrapped instanceof ParenthesizedExpression pe) {
unwrapped = pe.getExpression();
}
while (unwrapped instanceof CastExpression ce) {
unwrapped = ce.getExpression();
}
if (unwrapped instanceof ThisExpression) {
if (mi.getExpression() != null) {
results.addAll(getReachingDefinitions(mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1));
} else {
results.add(unwrapped);
}
} else {
results.addAll(getReachingDefinitions(retExpr, visited, newParamBindings, target.fieldBindings, depth + 1));
}
} }
} }
} }
@@ -284,11 +311,29 @@ public class JdtDataFlowModel implements DataFlowModel {
} }
private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(ClassInstanceCreation cic) { private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(ClassInstanceCreation cic) {
return buildFieldBindingsFromCic(cic, new HashMap<>(), new HashMap<>());
}
private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(
ClassInstanceCreation cic,
Map<IVariableBinding, Expression> callerParamBindings,
Map<IVariableBinding, Expression> callerInstanceFieldBindings) {
Map<IVariableBinding, Expression> fieldBindings = new HashMap<>(); Map<IVariableBinding, Expression> fieldBindings = new HashMap<>();
IMethodBinding cb = cic.resolveConstructorBinding(); IMethodBinding cb = cic.resolveConstructorBinding();
if (cb == null) return fieldBindings; if (cb == null) return fieldBindings;
evaluateConstructor(cb, cic.arguments(), new HashMap<>(), fieldBindings, 0); List<Expression> resolvedArguments = new ArrayList<>();
for (Object argObj : cic.arguments()) {
Expression arg = (Expression) argObj;
List<Expression> resolved = getReachingDefinitions(arg, new HashSet<>(), callerParamBindings, callerInstanceFieldBindings, 0);
if (!resolved.isEmpty()) {
resolvedArguments.add(resolved.get(0));
} else {
resolvedArguments.add(arg);
}
}
evaluateConstructor(cb, resolvedArguments, new HashMap<>(), fieldBindings, 0);
return fieldBindings; return fieldBindings;
} }
@@ -426,7 +471,26 @@ public class JdtDataFlowModel implements DataFlowModel {
if (concreteType != null) { if (concreteType != null) {
MethodDeclaration md = findMethodDeclarationInType(concreteType, mb); MethodDeclaration md = findMethodDeclarationInType(concreteType, mb);
if (md != null) { if (md != null) {
Map<IVariableBinding, Expression> concreteFieldBindings = buildFieldBindingsFromCic(cic); Map<IVariableBinding, Expression> concreteFieldBindings =
objectFieldBindings.computeIfAbsent(cic, k -> buildFieldBindingsFromCic((ClassInstanceCreation) k));
// Apply flow-sensitive intermediate local variable mutations
if (receiver instanceof SimpleName sn) {
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
if (enclosingMethod != null) {
ReachingDefinitions rd = getReachingDefinitionsForMethod(enclosingMethod);
if (rd != null) {
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
for (ASTNode def : defs) {
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
if (defExpr == cic || isExpressionWrapping(defExpr, cic)) {
applyIntermediateMutations(sn, def, mi, enclosingMethod, concreteFieldBindings, paramBindings, visited, depth);
}
}
}
}
}
targets.add(new TargetMethod(md, concreteFieldBindings)); targets.add(new TargetMethod(md, concreteFieldBindings));
} }
} }
@@ -456,6 +520,23 @@ public class JdtDataFlowModel implements DataFlowModel {
} }
} }
} }
// If the declaring class is a class (not an interface), or if the declared method itself is concrete/has a body,
// we should also include the declared method itself in the polymorphic targets.
MethodDeclaration declaredMd = findMethodDeclaration(mb);
if (declaredMd != null && declaredMd.getBody() != null) {
boolean alreadyAdded = false;
for (TargetMethod tm : targets) {
if (tm.declaration == declaredMd) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
targets.add(new TargetMethod(declaredMd, targetFieldBindings));
}
}
} }
// If no polymorphic subclass targets were found, fallback to the declared method itself // If no polymorphic subclass targets were found, fallback to the declared method itself
@@ -470,6 +551,218 @@ public class JdtDataFlowModel implements DataFlowModel {
return targets; return targets;
} }
private boolean isExpressionWrapping(Expression outer, Expression inner) {
Expression current = outer;
while (current != null) {
if (current == inner) return true;
if (current instanceof ParenthesizedExpression pe) {
current = pe.getExpression();
} else if (current instanceof CastExpression ce) {
current = ce.getExpression();
} else {
break;
}
}
return false;
}
private ControlFlowGraph.CfgNode findCfgNode(ControlFlowGraph cfg, ASTNode astNode) {
ASTNode current = astNode;
while (current != null) {
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
if (node.getAstNode() == current) {
return node;
}
}
current = current.getParent();
}
return null;
}
private boolean isTargetVariable(Expression expr, IVariableBinding targetVar, String targetName) {
if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding vb) {
if (targetVar != null && vb.getKey().equals(targetVar.getKey())) {
return true;
}
}
if (targetName != null && sn.getIdentifier().equals(targetName)) {
return true;
}
}
return false;
}
private void evaluateMethodMutations(
MethodDeclaration md,
List<?> arguments,
Map<IVariableBinding, Expression> fieldBindings) {
if (md == null || md.getBody() == null) return;
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
List<?> parameters = md.parameters();
int count = Math.min(parameters.size(), arguments.size());
for (int i = 0; i < count; i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
IVariableBinding paramVb = svd.resolveBinding();
if (paramVb != null) {
Expression arg = (Expression) arguments.get(i);
currentParamValues.put(paramVb, arg);
}
}
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
Expression rhs = assignment.getRightHandSide();
IVariableBinding fieldVb = null;
if (lhs instanceof FieldAccess fa) {
if (fa.getExpression() instanceof ThisExpression) {
IBinding b = fa.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
} else if (lhs instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
if (fieldVb != null) {
Expression resolvedRhs = resolveParamValue(rhs, currentParamValues);
fieldBindings.put(fieldVb, resolvedRhs);
}
return super.visit(assignment);
}
});
}
private void findAndApplyMutations(
ASTNode node,
IVariableBinding targetVar,
String targetName,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
node.accept(new ASTVisitor() {
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
Expression rhs = assignment.getRightHandSide();
IVariableBinding fieldVb = null;
if (lhs instanceof FieldAccess fa) {
if (isTargetVariable(fa.getExpression(), targetVar, targetName)) {
IBinding b = fa.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
} else if (lhs instanceof QualifiedName qn) {
if (isTargetVariable(qn.getQualifier(), targetVar, targetName)) {
IBinding b = qn.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
}
if (fieldVb != null) {
List<Expression> resolvedRhs = getReachingDefinitions(rhs, visited, paramBindings, fieldBindings, depth + 1);
if (!resolvedRhs.isEmpty()) {
fieldBindings.put(fieldVb, resolvedRhs.get(0));
} else {
fieldBindings.put(fieldVb, rhs);
}
}
return super.visit(assignment);
}
@Override
public boolean visit(MethodInvocation mi) {
Expression receiver = mi.getExpression();
if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) {
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1);
for (TargetMethod target : targets) {
if (target.declaration != null) {
evaluateMethodMutations(target.declaration, mi.arguments(), fieldBindings);
}
}
}
}
return super.visit(mi);
}
});
}
private void applyIntermediateMutations(
SimpleName receiverSimpleName,
ASTNode defNode,
ASTNode useNode,
MethodDeclaration enclosingMethod,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
IBinding b = receiverSimpleName.resolveBinding();
IVariableBinding targetVar = (b instanceof IVariableBinding vb) ? vb : null;
String targetName = receiverSimpleName.getIdentifier();
ControlFlowGraph cfg = cfgCache.computeIfAbsent(enclosingMethod, CfgBuilder::build);
if (cfg == null) return;
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, defNode);
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useNode);
if (defCfgNode != null && useCfgNode != null) {
Set<ControlFlowGraph.CfgNode> reachFromD = new HashSet<>();
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
queue.add(defCfgNode);
reachFromD.add(defCfgNode);
while (!queue.isEmpty()) {
ControlFlowGraph.CfgNode curr = queue.poll();
for (ControlFlowGraph.CfgNode succ : curr.getSuccessors()) {
if (reachFromD.add(succ)) {
queue.add(succ);
}
}
}
Set<ControlFlowGraph.CfgNode> reachToU = new HashSet<>();
queue.clear();
queue.add(useCfgNode);
reachToU.add(useCfgNode);
while (!queue.isEmpty()) {
ControlFlowGraph.CfgNode curr = queue.poll();
for (ControlFlowGraph.CfgNode pred : curr.getPredecessors()) {
if (reachToU.add(pred)) {
queue.add(pred);
}
}
}
Set<ControlFlowGraph.CfgNode> pathNodes = new HashSet<>(reachFromD);
pathNodes.retainAll(reachToU);
List<ControlFlowGraph.CfgNode> orderedPathNodes = new ArrayList<>(pathNodes);
List<ControlFlowGraph.CfgNode> allNodes = cfg.getNodes();
orderedPathNodes.sort(Comparator.comparingInt(allNodes::indexOf));
for (ControlFlowGraph.CfgNode node : orderedPathNodes) {
if (node != defCfgNode && node != useCfgNode && node.getAstNode() != null) {
findAndApplyMutations(node.getAstNode(), targetVar, targetName, fieldBindings, paramBindings, visited, depth);
}
}
}
}
private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) { private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) {
if (typeBinding == null || mb == null) return null; if (typeBinding == null || mb == null) return null;

View File

@@ -512,5 +512,166 @@ class JdtDataFlowModelTest {
String val = model.resolveValue(initEv, context); String val = model.resolveValue(initEv, context);
assertThat(val).isEqualTo("PAY"); assertThat(val).isEqualTo("PAY");
} }
@Test
void testFlowSensitiveLocalFieldMutations(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class Command {\n" +
" private String event = \"DEFAULT\";\n" +
" public void setEvent(String event) {\n" +
" this.event = event;\n" +
" }\n" +
" public String getEvent() {\n" +
" return this.event;\n" +
" }\n" +
" }\n" +
" public void run() {\n" +
" Command cmd = new Command();\n" +
" cmd.setEvent(\"PAY\");\n" +
" String res = cmd.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0);
Expression initializer = resFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("PAY");
}
@Test
void testFluentBuilderPatternPropagation(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class EventWrapper {\n" +
" private final String event;\n" +
" private EventWrapper(String event) {\n" +
" this.event = event;\n" +
" }\n" +
" public String getEvent() { return this.event; }\n" +
" public static Builder builder() {\n" +
" return new Builder();\n" +
" }\n" +
" public static class Builder {\n" +
" private String event;\n" +
" public Builder event(String event) {\n" +
" this.event = event;\n" +
" return this;\n" +
" }\n" +
" public EventWrapper build() {\n" +
" return new EventWrapper(this.event);\n" +
" }\n" +
" }\n" +
" }\n" +
" public void run() {\n" +
" EventWrapper w = EventWrapper.builder().event(\"PAY_SUCCESS\").build();\n" +
" String res = w.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0);
Expression initializer = resFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("PAY_SUCCESS");
}
@Test
void testPolymorphicUnionFallback(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public interface Service {\n" +
" String getEvent();\n" +
" }\n" +
" public static class PayService implements Service {\n" +
" public String getEvent() { return \"PAY\"; }\n" +
" }\n" +
" public static class CancelService implements Service {\n" +
" public String getEvent() { return \"CANCEL\"; }\n" +
" }\n" +
" public void run(Service s) {\n" +
" String ev = s.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement evStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0);
Expression initEv = evFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defs = model.getReachingDefinitions(initEv);
assertThat(defs).hasSize(2);
List<String> values = defs.stream()
.filter(e -> e instanceof StringLiteral)
.map(e -> ((StringLiteral) e).getLiteralValue())
.toList();
assertThat(values).containsExactlyInAnyOrder("PAY", "CANCEL");
}
} }