more reorg

This commit is contained in:
2026-06-27 14:50:06 +02:00
parent e8543ace38
commit 8a56e92a65
4 changed files with 760 additions and 714 deletions

View File

@@ -0,0 +1,620 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class StateMachineAdapterParser {
private final CodebaseContext context;
private enum AnalysisGoal {TRANSITIONS, STATES}
public record AdapterStates(Set<String> initialStates, Set<String> endStates) {}
public record AdapterConfig(
List<Transition> transitions,
Set<String> initialStates,
Set<String> endStates
) {}
public StateMachineAdapterParser(CodebaseContext context) {
this.context = context;
}
public AdapterConfig parse(TypeDeclaration startClass) {
List<Transition> transitions = parseTransitions(startClass);
AdapterStates states = parseStates(startClass);
return new AdapterConfig(transitions, states.initialStates(), states.endStates());
}
public List<Transition> parseTransitions(TypeDeclaration startClass) {
List<Transition> allTransitions = new ArrayList<>();
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
return allTransitions;
}
public AdapterStates parseStates(TypeDeclaration startClass) {
Set<String> initialStates = new HashSet<>();
Set<String> endStates = new HashSet<>();
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
return new AdapterStates(initialStates, endStates);
}
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureTransitionsMethod(method)) {
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null) {
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null) {
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
}
}
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
}
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit)) {
return new ArrayList<>();
}
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null) {
return new ArrayList<>();
}
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
}
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
if (expr instanceof MethodInvocation mi) {
if (isTransitionChainEntry(mi)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
} else if (isTransitionChainContinuation(mi, argsMap)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
} else if (isForEach(mi)) {
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
} else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression lambdaReceiver = mi.getExpression();
if (lambdaReceiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod == null) {
// Try to resolve method in another class if it's a static call or on a known type
Expression miReceiver = mi.getExpression();
if (miReceiver instanceof SimpleName sn) {
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
if (otherTd != null) {
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
if (calledMethod != null) {
// For calls to other classes, we switch the "searchStartClass" to that class
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
return transitions; // Handled
}
}
}
}
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
}
}
}
return transitions;
}
private boolean isForEach(MethodInvocation mi) {
return mi.getName().getIdentifier().equals("forEach");
}
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
List<?> params = lambda.parameters();
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
Object p = params.get(i);
String paramName = "";
if (p instanceof VariableDeclaration vp)
paramName = vp.getName().getIdentifier();
if (!paramName.isEmpty()) {
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
}
}
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
return Collections.emptyList();
}
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
Expression receiver = forEachMi.getExpression();
List<Expression> elements = unrollCollection(receiver, argsMap);
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
return transitions;
Object lambdaObj = forEachMi.arguments().get(0);
if (lambdaObj instanceof LambdaExpression lambda) {
String paramName = getLambdaParamName(lambda);
for (Expression element : elements) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
if (!paramName.isEmpty())
lambdaArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
else if (lambda.getBody() instanceof Expression lambdaExpr)
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
}
}
return transitions;
}
@SuppressWarnings("unchecked")
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
String paramName = efs.getParameter().getName().getIdentifier();
for (Expression element : elements) {
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
loopArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (efs.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
else if (efs.getBody() instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
} else {
if (efs.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
else if (efs.getBody() instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
}
}
return transitions;
}
@SuppressWarnings("unchecked")
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
Set<Expression> visited = new HashSet<>();
Expression current = expr;
while (current != null) {
if (!visited.add(current))
break;
if (current instanceof MethodInvocation mi) {
String name = mi.getName().getIdentifier();
Expression receiver = mi.getExpression();
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
if (mi.arguments().size() == 1) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
}
}
return (List<Expression>) mi.arguments();
}
}
if (current instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
if (resolved instanceof Expression e) {
current = e;
continue;
}
}
if (current instanceof ArrayInitializer ai)
return (List<Expression>) ai.expressions();
break;
}
return Collections.emptyList();
}
private String getLambdaParamName(LambdaExpression lambda) {
if (lambda.parameters().size() == 1) {
Object p = lambda.parameters().get(0);
if (p instanceof VariableDeclaration vp)
return vp.getName().getIdentifier();
}
return "";
}
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
else if (stmt instanceof TryStatement ts)
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
}
return transitions;
}
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else if (stmt instanceof TryStatement ts)
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (expr instanceof MethodInvocation mi) {
if (isWithStatesChain(mi, argsMap))
parseStateChain(mi, initialStates, endStates, argsMap);
else if (isForEach(mi))
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression receiver = mi.getExpression();
if (receiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
}
}
}
}
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
Map<String, Object> map = new HashMap<>(currentArgsMap);
List<?> params = method.parameters();
List<?> args = call.arguments();
for (int i = 0; i < params.size(); i++) {
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
String paramName = param.getName().getIdentifier();
if (param.isVarargs() && i < params.size()) {
List<Expression> varargList = new ArrayList<>();
for (int j = i; j < args.size(); j++) {
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
if (resolved instanceof List<?> list) {
for (Object item : list) {
if (item instanceof Expression e)
varargList.add(e);
}
} else if (resolved instanceof Expression e) {
varargList.add(e);
}
}
map.put(paramName, varargList);
} else if (i < args.size()) {
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
}
}
return map;
}
private Object resolve(Expression expr, Map<String, Object> argsMap) {
if (expr instanceof NullLiteral)
return null;
Set<Expression> visited = new HashSet<>();
Object current = expr;
while (current instanceof SimpleName sn) {
if (!visited.add((SimpleName) current))
break;
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved == null || resolved == current)
break;
current = resolved;
}
return current;
}
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
Object resolved = resolve(expr, argsMap);
if (resolved instanceof Expression e)
return e;
return expr;
}
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
Map<String, Expression> map = new HashMap<>();
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
if (entry.getValue() instanceof Expression e)
map.put(entry.getKey(), e);
}
return map;
}
private boolean isTransitionChainEntry(MethodInvocation mi) {
Expression current = mi;
while (current instanceof MethodInvocation call) {
String name = call.getName().getIdentifier();
if (TransitionType.fromMethodName(name).isPresent())
return true;
current = call.getExpression();
}
return false;
}
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
String name = call.getName().getIdentifier();
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
return resolved == null; // null means it's a parameter we track
}
if (TransitionType.fromMethodName(name).isPresent())
return true;
break;
}
return false;
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
return findMethodInHierarchy(methodName, td, new HashSet<>());
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
if (td == null)
return null;
String fqn = context.getFqn(td);
if (visited.contains(fqn))
return null;
visited.add(fqn);
for (MethodDeclaration method : td.getMethods())
if (method.getName().getIdentifier().equals(methodName))
return method;
Type superclassType = td.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
return findMethodInHierarchy(methodName, superclassTd, visited);
}
return null;
}
private boolean isRelevantMethod(MethodDeclaration method) {
return true;
}
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureTransitionsMethod(method))
return method;
return null;
}
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
}
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureStatesMethod(method)) {
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null)
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null)
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
}
}
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit))
return;
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null)
return;
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
if (call.getName().getIdentifier().equals("withStates"))
return true;
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
break;
}
return false;
}
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
List<MethodInvocation> chain = new ArrayList<>();
Expression current = mi;
while (current instanceof MethodInvocation m) {
chain.addFirst(m);
current = m.getExpression();
}
CompilationUnit cu = (CompilationUnit) mi.getRoot();
for (MethodInvocation call : chain) {
String methodName = call.getName().getIdentifier();
if (call.arguments().isEmpty())
continue;
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
if ("initial".equals(methodName)) {
if (!isInsideRegionOrFork(call)) {
initialStates.add(context.resolveState(arg, cu).toString());
}
} else if ("end".equals(methodName)) {
endStates.add(context.resolveState(arg, cu).toString());
}
}
}
private boolean isInsideRegionOrFork(MethodInvocation mi) {
Expression expr = mi.getExpression();
while (expr instanceof MethodInvocation parent) {
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
return true;
expr = parent.getExpression();
}
return false;
}
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureStatesMethod(method))
return method;
return null;
}
private boolean isConfigureStatesMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
}
private String getSimpleName(Type type) {
if (type.isSimpleType())
return ((SimpleType) type).getName().getFullyQualifiedName();
if (type.isParameterizedType())
return getSimpleName(((ParameterizedType) type).getType());
return type.toString();
}
}

View File

@@ -1,632 +1,37 @@
package click.kamil.springstatemachineexporter.ast.app; package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.model.Transition; import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.Getter; import lombok.Getter;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NullLiteral;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
public class StateMachineAggregator { public class StateMachineAggregator {
private final CodebaseContext context; private final CodebaseContext context;
private final StateMachineAdapterParser adapterParser;
private enum AnalysisGoal {TRANSITIONS, STATES}
public StateMachineAggregator(CodebaseContext context) {
this.context = context;
}
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
List<Transition> allTransitions = new ArrayList<>();
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
return allTransitions;
}
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureTransitionsMethod(method)) {
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null) {
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null) {
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
}
}
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
}
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit)) {
return new ArrayList<>();
}
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null) {
return new ArrayList<>();
}
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
}
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
if (expr instanceof MethodInvocation mi) {
if (isTransitionChainEntry(mi)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
} else if (isTransitionChainContinuation(mi, argsMap)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
} else if (isForEach(mi)) {
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
} else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression lambdaReceiver = mi.getExpression();
if (lambdaReceiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod == null) {
// Try to resolve method in another class if it's a static call or on a known type
Expression miReceiver = mi.getExpression();
if (miReceiver instanceof SimpleName sn) {
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
if (otherTd != null) {
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
if (calledMethod != null) {
// For calls to other classes, we switch the "searchStartClass" to that class
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
return transitions; // Handled
}
}
}
}
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
}
}
}
return transitions;
}
private boolean isForEach(MethodInvocation mi) {
return mi.getName().getIdentifier().equals("forEach");
}
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
List<?> params = lambda.parameters();
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
Object p = params.get(i);
String paramName = "";
if (p instanceof VariableDeclaration vp)
paramName = vp.getName().getIdentifier();
if (!paramName.isEmpty()) {
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
}
}
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
return Collections.emptyList();
}
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
Expression receiver = forEachMi.getExpression();
List<Expression> elements = unrollCollection(receiver, argsMap);
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
return transitions;
Object lambdaObj = forEachMi.arguments().get(0);
if (lambdaObj instanceof LambdaExpression lambda) {
String paramName = getLambdaParamName(lambda);
for (Expression element : elements) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
if (!paramName.isEmpty())
lambdaArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
else if (lambda.getBody() instanceof Expression lambdaExpr)
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
}
}
return transitions;
}
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
String paramName = efs.getParameter().getName().getIdentifier();
for (Expression element : elements) {
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
loopArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (efs.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
else if (efs.getBody() instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
} else {
if (efs.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
else if (efs.getBody() instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
}
}
return transitions;
}
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
Set<Expression> visited = new HashSet<>();
Expression current = expr;
while (current != null) {
if (!visited.add(current))
break;
if (current instanceof MethodInvocation mi) {
String name = mi.getName().getIdentifier();
Expression receiver = mi.getExpression();
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
if (mi.arguments().size() == 1) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
}
}
return (List<Expression>) mi.arguments();
}
}
if (current instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
if (resolved instanceof Expression e) {
current = e;
continue;
}
}
if (current instanceof ArrayInitializer ai)
return (List<Expression>) ai.expressions();
break;
}
return Collections.emptyList();
}
private String getLambdaParamName(LambdaExpression lambda) {
if (lambda.parameters().size() == 1) {
Object p = lambda.parameters().get(0);
if (p instanceof VariableDeclaration vp)
return vp.getName().getIdentifier();
}
return "";
}
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
else if (stmt instanceof TryStatement ts)
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
}
return transitions;
}
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else if (stmt instanceof TryStatement ts)
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (expr instanceof MethodInvocation mi) {
if (isWithStatesChain(mi, argsMap))
parseStateChain(mi, initialStates, endStates, argsMap);
else if (isForEach(mi))
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression receiver = mi.getExpression();
if (receiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
}
}
}
}
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
Map<String, Object> map = new HashMap<>(currentArgsMap);
List<?> params = method.parameters();
List<?> args = call.arguments();
for (int i = 0; i < params.size(); i++) {
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
String paramName = param.getName().getIdentifier();
if (param.isVarargs() && i < params.size()) {
List<Expression> varargList = new ArrayList<>();
for (int j = i; j < args.size(); j++) {
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
if (resolved instanceof List<?> list) {
for (Object item : list) {
if (item instanceof Expression e)
varargList.add(e);
}
} else if (resolved instanceof Expression e) {
varargList.add(e);
}
}
map.put(paramName, varargList);
} else if (i < args.size()) {
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
}
}
return map;
}
private Object resolve(Expression expr, Map<String, Object> argsMap) {
if (expr instanceof NullLiteral)
return null;
Set<Expression> visited = new HashSet<>();
Object current = expr;
while (current instanceof SimpleName sn) {
if (!visited.add((SimpleName) current))
break;
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved == null || resolved == current)
break;
current = resolved;
}
return current;
}
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
Object resolved = resolve(expr, argsMap);
if (resolved instanceof Expression e)
return e;
return expr;
}
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
Map<String, Expression> map = new HashMap<>();
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
if (entry.getValue() instanceof Expression e)
map.put(entry.getKey(), e);
}
return map;
}
private boolean isTransitionChainEntry(MethodInvocation mi) {
Expression current = mi;
while (current instanceof MethodInvocation call) {
String name = call.getName().getIdentifier();
if (TransitionType.fromMethodName(name).isPresent())
return true;
current = call.getExpression();
}
return false;
}
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
String name = call.getName().getIdentifier();
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
return resolved == null; // null means it's a parameter we track
}
if (TransitionType.fromMethodName(name).isPresent())
return true;
break;
}
return false;
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
return findMethodInHierarchy(methodName, td, new HashSet<>());
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
if (td == null)
return null;
String fqn = context.getFqn(td);
if (visited.contains(fqn))
return null;
visited.add(fqn);
for (MethodDeclaration method : td.getMethods())
if (method.getName().getIdentifier().equals(methodName))
return method;
Type superclassType = td.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
return findMethodInHierarchy(methodName, superclassTd, visited);
}
return null;
}
private boolean isRelevantMethod(MethodDeclaration method) {
return true;
}
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureTransitionsMethod(method))
return method;
return null;
}
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
}
@Getter @Getter
private final Set<String> initialStates = new HashSet<>(); private final Set<String> initialStates = new HashSet<>();
@Getter @Getter
private final Set<String> endStates = new HashSet<>(); private final Set<String> endStates = new HashSet<>();
public StateMachineAggregator(CodebaseContext context) {
this.context = context;
this.adapterParser = new StateMachineAdapterParser(context);
}
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
return adapterParser.parseTransitions(startClass);
}
public void aggregateStates(TypeDeclaration startClass) { public void aggregateStates(TypeDeclaration startClass) {
Set<MethodVisit> visitedMethods = new HashSet<>(); StateMachineAdapterParser.AdapterStates states = adapterParser.parseStates(startClass);
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>()); this.initialStates.clear();
} this.initialStates.addAll(states.initialStates());
this.endStates.clear();
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) { this.endStates.addAll(states.endStates());
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureStatesMethod(method)) {
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null)
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null)
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
}
}
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit))
return;
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null)
return;
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
if (call.getName().getIdentifier().equals("withStates"))
return true;
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
break;
}
return false;
}
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
List<MethodInvocation> chain = new ArrayList<>();
Expression current = mi;
while (current instanceof MethodInvocation m) {
chain.addFirst(m);
current = m.getExpression();
}
CompilationUnit cu = (CompilationUnit) mi.getRoot();
for (MethodInvocation call : chain) {
String methodName = call.getName().getIdentifier();
if (call.arguments().isEmpty())
continue;
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
if ("initial".equals(methodName)) {
if (!isInsideRegionOrFork(call)) {
initialStates.add(context.resolveState(arg, cu).toString());
}
} else if ("end".equals(methodName)) {
endStates.add(context.resolveState(arg, cu).toString());
}
}
}
private boolean isInsideRegionOrFork(MethodInvocation mi) {
Expression expr = mi.getExpression();
while (expr instanceof MethodInvocation parent) {
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
return true;
expr = parent.getExpression();
}
return false;
}
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureStatesMethod(method))
return method;
return null;
}
private boolean isConfigureStatesMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
}
private String getSimpleName(Type type) {
if (type.isSimpleType())
return ((SimpleType) type).getName().getFullyQualifiedName();
if (type.isParameterizedType())
return getSimpleName(((ParameterizedType) type).getType());
return type.toString();
} }
} }

View File

@@ -30,7 +30,12 @@ public class CodebaseContext {
private final Map<String, String> simpleNameToFqn = new HashMap<>(); private final Map<String, String> simpleNameToFqn = new HashMap<>();
private final Set<String> ambiguousSimpleNames = new HashSet<>(); private final Set<String> ambiguousSimpleNames = new HashSet<>();
private final ConstantResolver constantResolver = new ConstantResolver(); private final ConstantResolver constantResolver = new ConstantResolver();
private final StateResolver stateResolver = new StateResolver(this);
private final PropertyResolver propertyResolver = new PropertyResolver(); private final PropertyResolver propertyResolver = new PropertyResolver();
public ConstantResolver getConstantResolver() {
return constantResolver;
}
private final List<String> activeProfiles = new ArrayList<>(); private final List<String> activeProfiles = new ArrayList<>();
private Map<String, Map<String, String>> allProperties = new HashMap<>(); private Map<String, Map<String, String>> allProperties = new HashMap<>();
private List<LibraryHint> libraryHints = new ArrayList<>(); private List<LibraryHint> libraryHints = new ArrayList<>();
@@ -343,110 +348,7 @@ public class CodebaseContext {
} }
public State resolveState(Expression expr, CompilationUnit cu) { public State resolveState(Expression expr, CompilationUnit cu) {
if (expr == null) return stateResolver.resolveState(expr, cu);
return null;
// 1. Check for constants
String resolvedValue = constantResolver.resolve(expr, this);
if (resolvedValue != null) {
return State.of(expr.toString(), resolvedValue);
}
// 2. Check for @Value fields (SimpleName)
if (expr instanceof SimpleName sn) {
String fieldName = sn.getIdentifier();
TypeDeclaration td = findEnclosingType(sn);
if (td != null) {
String value = findValueFromField(td, fieldName);
if (value != null) {
return State.of(fieldName, value);
}
}
}
if (expr instanceof StringLiteral sl)
return State.of(sl.getLiteralValue());
String raw = expr.toString();
if (expr instanceof QualifiedName qn) {
String full = resolveQualifiedName(qn, cu);
return State.of(raw, full);
}
if (expr instanceof SimpleName sn) {
String full = resolveSimpleName(sn, cu);
return State.of(raw, full);
}
return State.of(raw);
}
private String findValueFromField(TypeDeclaration td, String fieldName) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(fieldName)) {
// Check for @Value annotation
for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("Value")) {
return extractAnnotationValue(ann);
}
}
}
}
}
}
}
return null;
}
private String extractAnnotationValue(Annotation ann) {
if (ann instanceof SingleMemberAnnotation sma) {
return stripQuotes(sma.getValue().toString());
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier())) {
return stripQuotes(pair.getValue().toString());
}
}
}
return null;
}
private String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
TypeDeclaration td = getTypeDeclaration(qualifier, cu);
if (td != null)
return getFqn(td) + "." + name;
return qn.getFullyQualifiedName();
}
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
String name = sn.getIdentifier();
if (cu == null)
return name;
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + name))
return impName;
}
return name;
} }
private CompilationUnit parse(String source, String unitName) { private CompilationUnit parse(String source, String unitName) {

View File

@@ -0,0 +1,119 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.model.State;
import org.eclipse.jdt.core.dom.*;
public class StateResolver {
private final CodebaseContext context;
public StateResolver(CodebaseContext context) {
this.context = context;
}
public State resolveState(Expression expr, CompilationUnit cu) {
if (expr == null)
return null;
// 1. Check for constants
String resolvedValue = context.getConstantResolver().resolve(expr, context);
if (resolvedValue != null) {
return State.of(expr.toString(), resolvedValue);
}
// 2. Check for @Value fields (SimpleName)
if (expr instanceof SimpleName sn) {
String fieldName = sn.getIdentifier();
TypeDeclaration td = findEnclosingType(sn);
if (td != null) {
String value = findValueFromField(td, fieldName);
if (value != null) {
return State.of(fieldName, value);
}
}
}
if (expr instanceof StringLiteral sl)
return State.of(sl.getLiteralValue());
String raw = expr.toString();
if (expr instanceof QualifiedName qn) {
String full = resolveQualifiedName(qn, cu);
return State.of(raw, full);
}
if (expr instanceof SimpleName sn) {
String full = resolveSimpleName(sn, cu);
return State.of(raw, full);
}
return State.of(raw);
}
private String findValueFromField(TypeDeclaration td, String fieldName) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(fieldName)) {
// Check for @Value annotation
for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("Value")) {
return extractAnnotationValue(ann);
}
}
}
}
}
}
}
return null;
}
private String extractAnnotationValue(Annotation ann) {
if (ann instanceof SingleMemberAnnotation sma) {
return stripQuotes(sma.getValue().toString());
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier())) {
return stripQuotes(pair.getValue().toString());
}
}
}
return null;
}
private String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
TypeDeclaration td = context.getTypeDeclaration(qualifier, cu);
if (td != null)
return context.getFqn(td) + "." + name;
return qn.getFullyQualifiedName();
}
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
String name = sn.getIdentifier();
if (cu == null)
return name;
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + name))
return impName;
}
return name;
}
}