This commit is contained in:
2026-06-27 19:18:32 +02:00
parent 5c7cf3639b
commit 76bb88e065
16 changed files with 1199 additions and 100 deletions

View File

@@ -185,9 +185,18 @@ public class ConstructorAnalyzer {
String tdKey = context.getFqn(td);
if (!visitedCtors.add(tdKey)) return;
IMethodBinding resolvedCtor = cic.resolveConstructorBinding();
for (MethodDeclaration ctor : td.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
if (ctor.parameters().size() != cic.arguments().size()) continue;
boolean matches = false;
if (resolvedCtor != null && ctor.resolveBinding() != null) {
matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey());
} else {
matches = ctor.parameters().size() == cic.arguments().size();
}
if (!matches) continue;
Map<String, String> paramValues = new HashMap<>();
for (int i = 0; i < ctor.parameters().size(); i++) {
@@ -247,11 +256,22 @@ public class ConstructorAnalyzer {
ctor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(SuperConstructorInvocation sci) {
IMethodBinding binding = sci.resolveConstructorBinding();
if (binding != null) {
ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
TypeDeclaration superTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
if (superTd != null) {
processSuperConstructorArgs(superTd, binding, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(sci);
}
}
}
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
processSuperConstructorArgs(superTd, null, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
@@ -259,7 +279,8 @@ public class ConstructorAnalyzer {
@Override
public boolean visit(ConstructorInvocation ci) {
processSuperConstructorArgs(td, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
IMethodBinding binding = ci.resolveConstructorBinding();
processSuperConstructorArgs(td, binding, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(ci);
}
});
@@ -269,6 +290,7 @@ public class ConstructorAnalyzer {
private void processSuperConstructorArgs(
TypeDeclaration superTd,
IMethodBinding resolvedBinding,
List<?> superArgs,
Map<String, String> subClassParamValues,
CodebaseContext context,
@@ -280,7 +302,14 @@ public class ConstructorAnalyzer {
for (MethodDeclaration superCtor : superTd.getMethods()) {
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
if (superCtor.parameters().size() != superArgs.size()) continue;
boolean matches = false;
if (resolvedBinding != null && superCtor.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(superCtor.resolveBinding()) || resolvedBinding.getKey().equals(superCtor.resolveBinding().getKey());
} else {
matches = superCtor.parameters().size() == superArgs.size();
}
if (!matches) continue;
Map<String, String> superParamValues = new HashMap<>();
for (int i = 0; i < superCtor.parameters().size(); i++) {
@@ -343,11 +372,22 @@ public class ConstructorAnalyzer {
superCtor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(SuperConstructorInvocation sci) {
IMethodBinding binding = sci.resolveConstructorBinding();
if (binding != null) {
ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
TypeDeclaration nextSuperTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
if (nextSuperTd != null) {
processSuperConstructorArgs(nextSuperTd, binding, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(sci);
}
}
}
String superFqn = context.getSuperclassFqn(superTd);
if (superFqn != null) {
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
if (nextSuperTd != null) {
processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
processSuperConstructorArgs(nextSuperTd, null, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
@@ -355,7 +395,8 @@ public class ConstructorAnalyzer {
@Override
public boolean visit(ConstructorInvocation ci) {
processSuperConstructorArgs(superTd, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
IMethodBinding binding = ci.resolveConstructorBinding();
processSuperConstructorArgs(superTd, binding, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(ci);
}
});

View File

@@ -11,9 +11,12 @@ public class VariableTracer {
private final ConstantResolver constantResolver;
private ConstantExtractor constantExtractor;
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
this.context = context;
this.constantResolver = constantResolver;
this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
}
public void setConstantExtractor(ConstantExtractor constantExtractor) {
@@ -21,101 +24,16 @@ public class VariableTracer {
}
public Expression traceVariable(Expression expr) {
List<Expression> all = traceVariableAll(expr, new HashSet<>());
List<Expression> all = traceVariableAll(expr);
return all.isEmpty() ? expr : all.get(all.size() - 1);
}
public List<Expression> traceVariableAll(Expression expr) {
return traceVariableAll(expr, new HashSet<>());
return dataFlowModel.getReachingDefinitions(expr);
}
public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
List<Expression> results = new ArrayList<>();
if (expr instanceof SimpleName sn) {
String varName = sn.getIdentifier();
String methodKey = "";
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
if (enclosingMethod != null) {
TypeDeclaration enclosingType = findEnclosingType(enclosingMethod);
if (enclosingType != null) {
methodKey = context.getFqn(enclosingType) + "." + enclosingMethod.getName().getIdentifier() + "#" + varName;
}
}
if (methodKey.isEmpty()) {
methodKey = varName;
}
if (!visitedVariables.add(methodKey)) {
return results; // Break infinite loop
}
if (enclosingMethod != null) {
enclosingMethod.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
results.addAll(traceVariableAll(node.getInitializer(), visitedVariables));
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
results.addAll(traceVariableAll(node.getRightHandSide(), visitedVariables));
}
return super.visit(node);
}
});
if (!results.isEmpty()) {
visitedVariables.remove(methodKey);
return results;
}
// Tracing parameter callers (Inter-procedural within the same class)
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
if (param.getName().getIdentifier().equals(varName)) {
int paramIndex = enclosingMethod.parameters().indexOf(param);
TypeDeclaration enclosingType = findEnclosingType(enclosingMethod);
if (enclosingType != null) {
String mName = enclosingMethod.getName().getIdentifier();
List<Expression> callerExprs = new ArrayList<>();
enclosingType.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (node.getName().getIdentifier().equals(mName) && node.arguments().size() > paramIndex) {
callerExprs.addAll(traceVariableAll((Expression) node.arguments().get(paramIndex), visitedVariables));
}
return super.visit(node);
}
@Override
public boolean visit(ExpressionMethodReference node) {
if (node.getName().getIdentifier().equals(mName)) {
if (node.getParent() instanceof MethodInvocation parentMi) {
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(parentMi)) {
callerExprs.addAll(traceVariableAll(parentMi.getExpression(), visitedVariables));
} else {
for (Object arg : parentMi.arguments()) {
if (arg != node) {
callerExprs.addAll(traceVariableAll((Expression) arg, visitedVariables));
}
}
}
}
}
return super.visit(node);
}
});
if (!callerExprs.isEmpty()) {
visitedVariables.remove(methodKey);
return callerExprs;
}
}
}
}
}
visitedVariables.remove(methodKey);
}
results.add(expr);
return results;
return dataFlowModel.getReachingDefinitions(expr);
}
public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {

View File

@@ -0,0 +1,101 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
public final class BindingResolver {
private BindingResolver() {
// Prevent instantiation
}
/**
* Resolves the type binding for a given AST Type node.
*/
public static ITypeBinding resolveType(Type type) {
if (type == null) return null;
return type.resolveBinding();
}
/**
* Resolves the type binding for a given AST Expression node.
*/
public static ITypeBinding resolveType(Expression expression) {
if (expression == null) return null;
return expression.resolveTypeBinding();
}
/**
* Resolves the method binding for a MethodInvocation.
*/
public static IMethodBinding resolveMethod(MethodInvocation invocation) {
if (invocation == null) return null;
return invocation.resolveMethodBinding();
}
/**
* Resolves the constructor binding for a ClassInstanceCreation.
*/
public static IMethodBinding resolveConstructor(ClassInstanceCreation creation) {
if (creation == null) return null;
return creation.resolveConstructorBinding();
}
/**
* Resolves the constructor binding for a ConstructorInvocation (e.g. this(...)).
*/
public static IMethodBinding resolveConstructor(ConstructorInvocation invocation) {
if (invocation == null) return null;
return invocation.resolveConstructorBinding();
}
/**
* Resolves the constructor binding for a SuperConstructorInvocation (e.g. super(...)).
*/
public static IMethodBinding resolveConstructor(SuperConstructorInvocation invocation) {
if (invocation == null) return null;
return invocation.resolveConstructorBinding();
}
/**
* Resolves the type binding of a TypeDeclaration.
*/
public static ITypeBinding resolveType(TypeDeclaration td) {
if (td == null) return null;
return td.resolveBinding();
}
/**
* Gets the fully qualified name of a type binding, safely handling nulls and erasure.
*/
public static String getFullyQualifiedName(ITypeBinding binding) {
if (binding == null) return null;
return binding.getErasure().getQualifiedName();
}
/**
* Checks if childBinding is compatible with (extends or implements) targetFqn.
*/
public static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn) {
if (childBinding == null || targetFqn == null) return false;
// Direct match
if (targetFqn.equals(childBinding.getErasure().getQualifiedName())) {
return true;
}
// Check interfaces
for (ITypeBinding intf : childBinding.getInterfaces()) {
if (isSubtypeOf(intf, targetFqn)) {
return true;
}
}
// Check superclass
ITypeBinding superclass = childBinding.getSuperclass();
if (superclass != null) {
return isSubtypeOf(superclass, targetFqn);
}
return false;
}
}

View File

@@ -0,0 +1,167 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class CfgBuilder {
public static ControlFlowGraph build(MethodDeclaration method) {
ControlFlowGraph cfg = new ControlFlowGraph(method);
if (method.getBody() == null) {
cfg.getEntryNode().addSuccessor(cfg.getExitNode());
return cfg;
}
// Build CFG recursively
List<ControlFlowGraph.CfgNode> exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg);
for (ControlFlowGraph.CfgNode exit : exits) {
exit.addSuccessor(cfg.getExitNode());
}
return cfg;
}
private static List<ControlFlowGraph.CfgNode> buildFlow(
ASTNode node,
List<ControlFlowGraph.CfgNode> sources,
ControlFlowGraph cfg) {
if (node == null || sources.isEmpty()) {
return sources;
}
if (node instanceof Block block) {
List<ControlFlowGraph.CfgNode> currentSources = sources;
for (Object stmtObj : block.statements()) {
currentSources = buildFlow((ASTNode) stmtObj, currentSources, cfg);
}
return currentSources;
}
if (node instanceof IfStatement ifStmt) {
// Condition node
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(ifStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(condNode);
}
List<ControlFlowGraph.CfgNode> thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg);
List<ControlFlowGraph.CfgNode> elseExits;
if (ifStmt.getElseStatement() != null) {
elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg);
} else {
elseExits = List.of(condNode);
}
List<ControlFlowGraph.CfgNode> combinedExits = new ArrayList<>();
combinedExits.addAll(thenExits);
combinedExits.addAll(elseExits);
return combinedExits;
}
if (node instanceof WhileStatement whileStmt) {
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(whileStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(condNode);
}
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(whileStmt.getBody(), List.of(condNode), cfg);
for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(condNode); // Loop back
}
return List.of(condNode); // Exit loop when cond is false
}
if (node instanceof DoStatement doStmt) {
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), sources, cfg);
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(condNode);
}
condNode.addSuccessor(condNode); // Loop back
return List.of(condNode);
}
if (node instanceof ForStatement forStmt) {
// Initializers
List<ControlFlowGraph.CfgNode> currentSources = sources;
for (Object initObj : forStmt.initializers()) {
ControlFlowGraph.CfgNode initNode = new ControlFlowGraph.CfgNode((ASTNode) initObj, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(initNode);
for (ControlFlowGraph.CfgNode src : currentSources) {
src.addSuccessor(initNode);
}
currentSources = List.of(initNode);
}
// Condition
ControlFlowGraph.CfgNode condNode;
if (forStmt.getExpression() != null) {
condNode = new ControlFlowGraph.CfgNode(forStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
} else {
condNode = new ControlFlowGraph.CfgNode(forStmt, ControlFlowGraph.CfgNodeType.STATEMENT); // Dummy node
}
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode src : currentSources) {
src.addSuccessor(condNode);
}
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg);
// Updaters
List<ControlFlowGraph.CfgNode> updaterSources = bodyExits;
for (Object updateObj : forStmt.updaters()) {
ControlFlowGraph.CfgNode updateNode = new ControlFlowGraph.CfgNode((ASTNode) updateObj, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(updateNode);
for (ControlFlowGraph.CfgNode src : updaterSources) {
src.addSuccessor(updateNode);
}
updaterSources = List.of(updateNode);
}
for (ControlFlowGraph.CfgNode src : updaterSources) {
src.addSuccessor(condNode); // Loop back
}
return List.of(condNode);
}
if (node instanceof EnhancedForStatement effStmt) {
ControlFlowGraph.CfgNode loopNode = new ControlFlowGraph.CfgNode(effStmt.getParameter(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(loopNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(loopNode);
}
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(effStmt.getBody(), List.of(loopNode), cfg);
for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(loopNode); // Loop back
}
return List.of(loopNode);
}
if (node instanceof ReturnStatement returnStmt) {
ControlFlowGraph.CfgNode retNode = new ControlFlowGraph.CfgNode(returnStmt, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(retNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(retNode);
}
// Return flows to exit, not to subsequent sequential nodes
retNode.addSuccessor(cfg.getExitNode());
return Collections.emptyList();
}
// Default: general statement
ControlFlowGraph.CfgNode stmtNode = new ControlFlowGraph.CfgNode(node, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(stmtNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(stmtNode);
}
return List.of(stmtNode);
}
}

View File

@@ -356,6 +356,7 @@ public class CodebaseContext {
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(resolveBindings);
parser.setBindingsRecovery(true);
Map<String, String> options = JavaCore.getOptions();
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
@@ -445,6 +446,14 @@ public class CodebaseContext {
}
public String getSuperclassFqn(TypeDeclaration td) {
ITypeBinding binding = td.resolveBinding();
if (binding != null) {
ITypeBinding superBinding = binding.getSuperclass();
if (superBinding != null) {
return superBinding.getErasure().getQualifiedName();
}
}
Type superType = td.getSuperclassType();
if (superType == null) return null;
@@ -466,6 +475,44 @@ public class CodebaseContext {
}
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
ITypeBinding binding = td.resolveBinding();
if (binding != null && includeSuper) {
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
if (methodBinding != null) {
CompilationUnit declaringCu = classes.get(methodBinding.getDeclaringClass().getErasure().getQualifiedName());
if (declaringCu != null) {
ASTNode declNode = declaringCu.findDeclaringNode(methodBinding.getKey());
if (declNode instanceof MethodDeclaration) {
return (MethodDeclaration) declNode;
}
}
}
}
return findMethodDeclarationLegacy(td, methodName, includeSuper);
}
private IMethodBinding findMethodBinding(ITypeBinding typeBinding, String methodName) {
for (IMethodBinding mb : typeBinding.getDeclaredMethods()) {
if (mb.getName().equals(methodName)) {
return mb;
}
}
ITypeBinding superclass = typeBinding.getSuperclass();
if (superclass != null) {
IMethodBinding mb = findMethodBinding(superclass, methodName);
if (mb != null) return mb;
}
for (ITypeBinding intf : typeBinding.getInterfaces()) {
IMethodBinding mb = findMethodBinding(intf, methodName);
if (mb != null) return mb;
}
return null;
}
private MethodDeclaration findMethodDeclarationLegacy(TypeDeclaration td, String methodName, boolean includeSuper) {
for (MethodDeclaration method : td.getMethods()) {
if (method.getName().getIdentifier().equals(methodName)) {
return method;
@@ -477,7 +524,7 @@ public class CodebaseContext {
if (superFqn != null) {
TypeDeclaration superTd = getTypeDeclaration(superFqn);
if (superTd != null) {
return findMethodDeclaration(superTd, methodName, true);
return findMethodDeclarationLegacy(superTd, methodName, true);
}
}
@@ -487,7 +534,7 @@ public class CodebaseContext {
String interfaceName = extractTypeName(interfaceType);
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
if (interfaceTd != null) {
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
if (found != null) return found;
}
}
@@ -665,10 +712,19 @@ public class CodebaseContext {
}
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
ITypeBinding binding = td.resolveBinding();
if (binding != null) {
boolean isSubtype = BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.builders.StateMachineConfigurer")
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.StateMachineConfigurerAdapter")
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter");
if (isSubtype) {
return true;
}
}
return extendsStateMachineConfigurerAdapterLegacy(td, new HashSet<>());
}
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td, Set<String> visited) {
private boolean extendsStateMachineConfigurerAdapterLegacy(TypeDeclaration td, Set<String> visited) {
String fqn = getFqn(td);
if (visited.contains(fqn)) {
return false;
@@ -685,7 +741,7 @@ public class CodebaseContext {
if (knownAdapters.contains(superclassName))
return true;
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
return true;
}
@@ -696,7 +752,7 @@ public class CodebaseContext {
if (knownAdapters.contains(interfaceName))
return true;
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
if (interfaceTd != null && extendsStateMachineConfigurerAdapterLegacy(interfaceTd, visited))
return true;
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class ControlFlowGraph {
private final MethodDeclaration method;
private final CfgNode entryNode = new CfgNode(null, CfgNodeType.ENTRY);
private final CfgNode exitNode = new CfgNode(null, CfgNodeType.EXIT);
private final List<CfgNode> nodes = new ArrayList<>();
public enum CfgNodeType {
ENTRY,
EXIT,
STATEMENT
}
public static class CfgNode {
private final ASTNode astNode;
private final CfgNodeType type;
private final Set<CfgNode> predecessors = new HashSet<>();
private final Set<CfgNode> successors = new HashSet<>();
public CfgNode(ASTNode astNode, CfgNodeType type) {
this.astNode = astNode;
this.type = type;
}
public ASTNode getAstNode() {
return astNode;
}
public CfgNodeType getType() {
return type;
}
public Set<CfgNode> getPredecessors() {
return predecessors;
}
public Set<CfgNode> getSuccessors() {
return successors;
}
public void addSuccessor(CfgNode succ) {
if (succ != null) {
this.successors.add(succ);
succ.predecessors.add(this);
}
}
}
public ControlFlowGraph(MethodDeclaration method) {
this.method = method;
this.nodes.add(entryNode);
this.nodes.add(exitNode);
}
public MethodDeclaration getMethod() {
return method;
}
public CfgNode getEntryNode() {
return entryNode;
}
public CfgNode getExitNode() {
return exitNode;
}
public List<CfgNode> getNodes() {
return nodes;
}
public void addNode(CfgNode node) {
this.nodes.add(node);
}
}

View File

@@ -0,0 +1,18 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.Expression;
import java.util.List;
public interface DataFlowModel {
/**
* Resolves all possible Expressions that define the value of the given expression
* at its location in the control flow.
*/
List<Expression> getReachingDefinitions(Expression expr);
/**
* Resolves the single constant/literal value if all definitions converge,
* or if a single value is expected.
*/
String resolveValue(Expression expr, CodebaseContext context);
}

View File

@@ -0,0 +1,89 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class JdtDataFlowModel implements DataFlowModel {
private final CodebaseContext context;
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
public JdtDataFlowModel(CodebaseContext context) {
this.context = context;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof MethodDeclaration) {
return (MethodDeclaration) current;
}
current = current.getParent();
}
return null;
}
private ReachingDefinitions getReachingDefinitionsForMethod(MethodDeclaration method) {
if (method == null) return null;
return rdCache.computeIfAbsent(method, m -> {
ControlFlowGraph cfg = cfgCache.computeIfAbsent(m, CfgBuilder::build);
return new ReachingDefinitions(cfg);
});
}
@Override
public List<Expression> getReachingDefinitions(Expression expr) {
return getReachingDefinitions(expr, new HashSet<>());
}
private List<Expression> getReachingDefinitions(Expression expr, Set<ASTNode> visited) {
if (expr == null || !visited.add(expr)) return List.of();
if (expr instanceof SimpleName sn) {
MethodDeclaration md = findEnclosingMethod(sn);
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
if (rd != null) {
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
if (!defs.isEmpty()) {
List<Expression> exprs = new ArrayList<>();
for (ASTNode def : defs) {
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
if (defExpr != null) {
exprs.addAll(getReachingDefinitions(defExpr, visited));
} else {
exprs.add(expr);
}
}
visited.remove(expr);
return exprs;
}
}
}
visited.remove(expr);
return List.of(expr);
}
@Override
public String resolveValue(Expression expr, CodebaseContext context) {
if (expr == null) return null;
List<Expression> defs = getReachingDefinitions(expr);
if (defs.isEmpty()) {
return context.resolveExpression(expr);
}
String firstVal = null;
for (Expression def : defs) {
String val = context.resolveExpression(def);
if (val != null) {
if (firstVal == null) {
firstVal = val;
} else if (!firstVal.equals(val)) {
return null;
}
}
}
return firstVal != null ? firstVal : context.resolveExpression(expr);
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.Expression;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import java.util.List;
public class LegacyDataFlowModel implements DataFlowModel {
private final VariableTracer variableTracer;
public LegacyDataFlowModel(VariableTracer variableTracer) {
this.variableTracer = variableTracer;
}
@Override
public List<Expression> getReachingDefinitions(Expression expr) {
if (variableTracer == null || expr == null) {
return expr != null ? List.of(expr) : List.of();
}
return variableTracer.traceVariableAll(expr);
}
@Override
public String resolveValue(Expression expr, CodebaseContext context) {
if (expr == null) return null;
if (variableTracer == null) {
return context.resolveExpression(expr);
}
Expression traced = variableTracer.traceVariable(expr);
return context.resolveExpression(traced);
}
}

View File

@@ -0,0 +1,148 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class ReachingDefinitions {
private final ControlFlowGraph cfg;
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> inMap = new HashMap<>();
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> outMap = new HashMap<>();
public ReachingDefinitions(ControlFlowGraph cfg) {
this.cfg = cfg;
analyze();
}
private void analyze() {
List<ControlFlowGraph.CfgNode> cfgNodes = cfg.getNodes();
Map<ControlFlowGraph.CfgNode, ASTNode> nodeDefs = new HashMap<>();
Map<String, Set<ASTNode>> varToDefs = new HashMap<>();
for (ControlFlowGraph.CfgNode node : cfgNodes) {
ASTNode ast = node.getAstNode();
if (ast == null) continue;
ASTNode def = findDefinition(ast);
if (def != null) {
nodeDefs.put(node, def);
String varName = getDefinedVariableName(def);
if (varName != null) {
varToDefs.computeIfAbsent(varName, k -> new HashSet<>()).add(def);
}
}
}
for (ControlFlowGraph.CfgNode node : cfgNodes) {
inMap.put(node, new HashSet<>());
outMap.put(node, new HashSet<>());
}
boolean changed = true;
while (changed) {
changed = false;
for (ControlFlowGraph.CfgNode node : cfgNodes) {
Set<ASTNode> newIn = new HashSet<>();
for (ControlFlowGraph.CfgNode pred : node.getPredecessors()) {
newIn.addAll(outMap.get(pred));
}
inMap.put(node, newIn);
Set<ASTNode> newOut = new HashSet<>(newIn);
ASTNode def = nodeDefs.get(node);
if (def != null) {
String varName = getDefinedVariableName(def);
if (varName != null) {
Set<ASTNode> allDefs = varToDefs.get(varName);
if (allDefs != null) {
newOut.removeAll(allDefs);
}
}
newOut.add(def);
}
Set<ASTNode> oldOut = outMap.put(node, newOut);
if (!newOut.equals(oldOut)) {
changed = true;
}
}
}
}
public Set<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) {
ControlFlowGraph.CfgNode cfgNode = findCfgNodeForUsage(usageNode);
if (cfgNode == null) {
return Collections.emptySet();
}
Set<ASTNode> reaching = new HashSet<>();
Set<ASTNode> inDefs = inMap.get(cfgNode);
if (inDefs != null) {
for (ASTNode def : inDefs) {
if (varName.equals(getDefinedVariableName(def))) {
reaching.add(def);
}
}
}
return reaching;
}
private ControlFlowGraph.CfgNode findCfgNodeForUsage(ASTNode usageNode) {
ASTNode current = usageNode;
while (current != null) {
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
if (node.getAstNode() == current) {
return node;
}
}
current = current.getParent();
}
return null;
}
private ASTNode findDefinition(ASTNode ast) {
if (ast instanceof VariableDeclarationStatement vds) {
if (!vds.fragments().isEmpty()) {
return (ASTNode) vds.fragments().get(0);
}
}
if (ast instanceof VariableDeclarationFragment) {
return ast;
}
if (ast instanceof SingleVariableDeclaration) {
return ast;
}
if (ast instanceof Assignment) {
return ast;
}
if (ast instanceof ExpressionStatement es) {
return findDefinition(es.getExpression());
}
return null;
}
public static String getDefinedVariableName(ASTNode def) {
if (def instanceof VariableDeclarationFragment frag) {
return frag.getName().getIdentifier();
}
if (def instanceof SingleVariableDeclaration svd) {
return svd.getName().getIdentifier();
}
if (def instanceof Assignment assign) {
if (assign.getLeftHandSide() instanceof SimpleName sn) {
return sn.getIdentifier();
}
}
return null;
}
public static Expression getDefinitionExpression(ASTNode def) {
if (def instanceof VariableDeclarationFragment frag) {
return frag.getInitializer();
}
if (def instanceof Assignment assign) {
return assign.getRightHandSide();
}
return null;
}
}

View File

@@ -9,6 +9,7 @@ import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -88,4 +89,42 @@ public class ResolutionRobustnessTest {
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
}
@Test
void testJdtBindingsResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Base.java"),
"package com.foo;\n" +
"public class Base {\n" +
" public void run() {}\n" +
"}"
);
Files.writeString(comFoo.resolve("Child.java"),
"package com.foo;\n" +
"public class Child extends Base {\n" +
" public void execute() {\n" +
" run();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
assertThat(childTd).isNotNull();
// Resolve the binding for Child class
org.eclipse.jdt.core.dom.ITypeBinding binding = childTd.resolveBinding();
assertThat(binding).isNotNull();
assertThat(binding.getQualifiedName()).isEqualTo("com.foo.Child");
// Verify we can get the superclass binding
org.eclipse.jdt.core.dom.ITypeBinding superBinding = binding.getSuperclass();
assertThat(superBinding).isNotNull();
assertThat(superBinding.getQualifiedName()).isEqualTo("com.foo.Base");
}
}

View File

@@ -0,0 +1,92 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class BindingResolverTest {
@Test
void testBindingResolverMethods(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Base.java"),
"package com.foo;\n" +
"public interface Base {\n" +
" void run();\n" +
"}"
);
Files.writeString(comFoo.resolve("Child.java"),
"package com.foo;\n" +
"public class Child implements Base {\n" +
" public void run() {}\n" +
" public void execute() {\n" +
" Child c = new Child();\n" +
" c.run();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
assertThat(childTd).isNotNull();
ITypeBinding binding = BindingResolver.resolveType(childTd);
assertThat(binding).isNotNull();
assertThat(BindingResolver.getFullyQualifiedName(binding)).isEqualTo("com.foo.Child");
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.Base")).isTrue();
assertThat(BindingResolver.isSubtypeOf(binding, "java.lang.Object")).isTrue();
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.NonExistent")).isFalse();
// Find MethodDeclaration execute
MethodDeclaration executeMethod = null;
for (MethodDeclaration md : childTd.getMethods()) {
if (md.getName().getIdentifier().equals("execute")) {
executeMethod = md;
}
}
assertThat(executeMethod).isNotNull();
// Traverse the body to find ClassInstanceCreation and MethodInvocation
class Visitor extends ASTVisitor {
ClassInstanceCreation cic = null;
MethodInvocation mi = null;
@Override
public boolean visit(ClassInstanceCreation node) {
cic = node;
return super.visit(node);
}
@Override
public boolean visit(MethodInvocation node) {
mi = node;
return super.visit(node);
}
}
Visitor visitor = new Visitor();
executeMethod.accept(visitor);
assertThat(visitor.cic).isNotNull();
IMethodBinding constrBinding = BindingResolver.resolveConstructor(visitor.cic);
assertThat(constrBinding).isNotNull();
assertThat(constrBinding.isConstructor()).isTrue();
assertThat(visitor.mi).isNotNull();
IMethodBinding methodBinding = BindingResolver.resolveMethod(visitor.mi);
assertThat(methodBinding).isNotNull();
assertThat(methodBinding.getName()).isEqualTo("run");
}
}

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ControlFlowGraphTest {
@Test
void testSequentialAndBranchingCfg(@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 void run(boolean flag) {\n" +
" int a = 1;\n" +
" if (flag) {\n" +
" a = 2;\n" +
" } else {\n" +
" a = 3;\n" +
" }\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
assertThat(cfg).isNotNull();
assertThat(cfg.getEntryNode().getSuccessors()).hasSize(1);
// Entry goes to first statement: 'int a = 1;'
ControlFlowGraph.CfgNode firstStmtNode = cfg.getEntryNode().getSuccessors().iterator().next();
assertThat(firstStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
// First stmt goes to condition of 'if (flag)'
assertThat(firstStmtNode.getSuccessors()).hasSize(1);
ControlFlowGraph.CfgNode condNode = firstStmtNode.getSuccessors().iterator().next();
assertThat(condNode.getAstNode()).isInstanceOf(SimpleName.class); // flag
// Cond node goes to two branches: 'a = 2;' and 'a = 3;'
assertThat(condNode.getSuccessors()).hasSize(2);
for (ControlFlowGraph.CfgNode branchSuccessor : condNode.getSuccessors()) {
assertThat(branchSuccessor.getAstNode()).isInstanceOf(ExpressionStatement.class); // assignment expression statement
// Each branch successor should flow into the next sequential statement: 'int b = a;'
assertThat(branchSuccessor.getSuccessors()).hasSize(1);
ControlFlowGraph.CfgNode nextStmtNode = branchSuccessor.getSuccessors().iterator().next();
assertThat(nextStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
assertThat(nextStmtNode.getSuccessors()).contains(cfg.getExitNode());
}
}
}

View File

@@ -0,0 +1,91 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class JdtDataFlowModelTest {
@Test
void testRecursiveJdtDataFlowResolution(@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 void run() {\n" +
" String a = \"VAL1\";\n" +
" String b = a;\n" +
" String c = b;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement cStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment cFrag = (VariableDeclarationFragment) cStmt.fragments().get(0);
Expression initializer = cFrag.getInitializer(); // b
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
Expression singleDef = defs.get(defs.size() - 1);
assertThat(singleDef).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) singleDef).getLiteralValue()).isEqualTo("VAL1");
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("VAL1");
}
@Test
void testBranchDivergenceJdtDataFlowResolution(@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 void run(boolean flag) {\n" +
" String a = \"SAME\";\n" +
" if (flag) {\n" +
" a = \"SAME\";\n" +
" } else {\n" +
" a = \"SAME\";\n" +
" }\n" +
" String b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer(); // a
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
// Both branches assign "SAME", so it converges to "SAME"
assertThat(val).isEqualTo("SAME");
}
}

View File

@@ -0,0 +1,56 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class LegacyDataFlowModelTest {
@Test
void testLegacyDataFlowResolution(@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 void run() {\n" +
" String a = \"VAL1\";\n" +
" String b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment frag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = frag.getInitializer(); // 'a' reference
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
DataFlowModel model = new LegacyDataFlowModel(tracer);
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
Expression resolvedExpr = defs.get(defs.size() - 1);
assertThat(resolvedExpr).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) resolvedExpr).getLiteralValue()).isEqualTo("VAL1");
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("VAL1");
}
}

View File

@@ -0,0 +1,105 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
class ReachingDefinitionsTest {
@Test
void testReachingDefinitionsBranchMerge(@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 void run(boolean flag) {\n" +
" int a = 1;\n" +
" if (flag) {\n" +
" a = 2;\n" +
" } else {\n" +
" a = 3;\n" +
" }\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
ReachingDefinitions rd = new ReachingDefinitions(cfg);
// Find usage 'a' in 'int b = a;'
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer(); // simpleName 'a'
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
assertThat(defs).hasSize(2);
Set<String> values = defs.stream()
.map(ReachingDefinitions::getDefinitionExpression)
.map(expr -> {
if (expr instanceof NumberLiteral nl) {
return nl.getToken();
}
return expr.toString();
})
.collect(Collectors.toSet());
assertThat(values).containsExactlyInAnyOrder("2", "3");
}
@Test
void testReachingDefinitionsKilled(@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 void run() {\n" +
" int a = 1;\n" +
" a = 2;\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
ReachingDefinitions rd = new ReachingDefinitions(cfg);
// Find usage 'a' in 'int b = a;'
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer();
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
assertThat(defs).hasSize(1);
ASTNode singleDef = defs.iterator().next();
Expression defExpr = ReachingDefinitions.getDefinitionExpression(singleDef);
assertThat(defExpr).isInstanceOf(NumberLiteral.class);
assertThat(((NumberLiteral) defExpr).getToken()).isEqualTo("2");
}
}