diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java index c175aec..4bbd004 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java @@ -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 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 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 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); } }); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java index 72762da..28aad47 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java @@ -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 all = traceVariableAll(expr, new HashSet<>()); + List all = traceVariableAll(expr); return all.isEmpty() ? expr : all.get(all.size() - 1); } public List traceVariableAll(Expression expr) { - return traceVariableAll(expr, new HashSet<>()); + return dataFlowModel.getReachingDefinitions(expr); } public List traceVariableAll(Expression expr, Set visitedVariables) { - List 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 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) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/BindingResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/BindingResolver.java new file mode 100644 index 0000000..1117dd6 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/BindingResolver.java @@ -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; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java new file mode 100644 index 0000000..920a0b8 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java @@ -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 exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg); + for (ControlFlowGraph.CfgNode exit : exits) { + exit.addSuccessor(cfg.getExitNode()); + } + + return cfg; + } + + private static List buildFlow( + ASTNode node, + List sources, + ControlFlowGraph cfg) { + + if (node == null || sources.isEmpty()) { + return sources; + } + + if (node instanceof Block block) { + List 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 thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg); + List elseExits; + if (ifStmt.getElseStatement() != null) { + elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg); + } else { + elseExits = List.of(condNode); + } + + List 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 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 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 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 bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg); + + // Updaters + List 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 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); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 93d008f..be0af46 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -356,6 +356,7 @@ public class CodebaseContext { parser.setSource(source.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); parser.setResolveBindings(resolveBindings); + parser.setBindingsRecovery(true); Map 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 visited) { + private boolean extendsStateMachineConfigurerAdapterLegacy(TypeDeclaration td, Set 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; } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ControlFlowGraph.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ControlFlowGraph.java new file mode 100644 index 0000000..013e63e --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ControlFlowGraph.java @@ -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 nodes = new ArrayList<>(); + + public enum CfgNodeType { + ENTRY, + EXIT, + STATEMENT + } + + public static class CfgNode { + private final ASTNode astNode; + private final CfgNodeType type; + private final Set predecessors = new HashSet<>(); + private final Set 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 getPredecessors() { + return predecessors; + } + + public Set 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 getNodes() { + return nodes; + } + + public void addNode(CfgNode node) { + this.nodes.add(node); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/DataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/DataFlowModel.java new file mode 100644 index 0000000..034bbec --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/DataFlowModel.java @@ -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 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); +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java new file mode 100644 index 0000000..66d1c77 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java @@ -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 cfgCache = new HashMap<>(); + private final Map 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 getReachingDefinitions(Expression expr) { + return getReachingDefinitions(expr, new HashSet<>()); + } + + private List getReachingDefinitions(Expression expr, Set 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 defs = rd.getReachingDefinitions(sn, sn.getIdentifier()); + if (!defs.isEmpty()) { + List 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 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); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModel.java new file mode 100644 index 0000000..c863946 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModel.java @@ -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 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); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java new file mode 100644 index 0000000..a375372 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java @@ -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> inMap = new HashMap<>(); + private final Map> outMap = new HashMap<>(); + + public ReachingDefinitions(ControlFlowGraph cfg) { + this.cfg = cfg; + analyze(); + } + + private void analyze() { + List cfgNodes = cfg.getNodes(); + Map nodeDefs = new HashMap<>(); + Map> 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 newIn = new HashSet<>(); + for (ControlFlowGraph.CfgNode pred : node.getPredecessors()) { + newIn.addAll(outMap.get(pred)); + } + + inMap.put(node, newIn); + + Set newOut = new HashSet<>(newIn); + ASTNode def = nodeDefs.get(node); + if (def != null) { + String varName = getDefinedVariableName(def); + if (varName != null) { + Set allDefs = varToDefs.get(varName); + if (allDefs != null) { + newOut.removeAll(allDefs); + } + } + newOut.add(def); + } + + Set oldOut = outMap.put(node, newOut); + if (!newOut.equals(oldOut)) { + changed = true; + } + } + } + } + + public Set getReachingDefinitions(ASTNode usageNode, String varName) { + ControlFlowGraph.CfgNode cfgNode = findCfgNodeForUsage(usageNode); + if (cfgNode == null) { + return Collections.emptySet(); + } + + Set reaching = new HashSet<>(); + Set 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; + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ResolutionRobustnessTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ResolutionRobustnessTest.java index 1f02cfa..112fec1 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ResolutionRobustnessTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ResolutionRobustnessTest.java @@ -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"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/BindingResolverTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/BindingResolverTest.java new file mode 100644 index 0000000..65c6f08 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/BindingResolverTest.java @@ -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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/ControlFlowGraphTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/ControlFlowGraphTest.java new file mode 100644 index 0000000..2920f74 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/ControlFlowGraphTest.java @@ -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()); + } + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java new file mode 100644 index 0000000..25e6450 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java @@ -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 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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModelTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModelTest.java new file mode 100644 index 0000000..dfdfe43 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModelTest.java @@ -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 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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitionsTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitionsTest.java new file mode 100644 index 0000000..211c460 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitionsTest.java @@ -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 defs = rd.getReachingDefinitions(initializer, "a"); + assertThat(defs).hasSize(2); + + Set 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 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"); + } +}