3 Commits

11 changed files with 581 additions and 92 deletions

View File

@@ -59,7 +59,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
.targetState(smSourceRaw) .targetState(smSourceRaw)
.event(smEventRaw) .event(smEventRaw)
.build(); .build();
if (isRoutedToCorrectMachine(chain, result.getName())) { if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
matched.add(mt); matched.add(mt);
} }
} else { } else {
@@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
.targetState(targetRaw) .targetState(targetRaw)
.event(smEventRaw) .event(smEventRaw)
.build(); .build();
if (isRoutedToCorrectMachine(chain, result.getName())) { if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
matched.add(mt); matched.add(mt);
} }
} }
@@ -100,8 +100,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
} }
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) { private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName); return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context);
} }
private String simplify(String name) { private String simplify(String name) {

View File

@@ -37,7 +37,8 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
String simplifiedPe = simplify(simplePe); String simplifiedPe = simplify(simplePe);
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) || if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) { simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) ||
isGuardedContains(smEvent, simplifiedPe)) {
hasPolyMatch = true; hasPolyMatch = true;
break; break;
} }
@@ -54,7 +55,38 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
} }
String triggerEvent = simplify(rawTriggerEvent); String triggerEvent = simplify(rawTriggerEvent);
return smEvent.equals(triggerEvent); if (smEvent.equalsIgnoreCase(triggerEvent)) {
return true;
}
return isGuardedContains(smEvent, triggerEvent);
}
private boolean isGuardedContains(String smEvent, String triggerEvent) {
if (smEvent == null || triggerEvent == null) return false;
// Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
return false;
}
String smLower = smEvent.toLowerCase();
String triggerLower = triggerEvent.toLowerCase();
if (smLower.equals(triggerLower)) {
return true;
}
// Match if one contains the other as a whole word/segment.
// We quote the patterns to avoid regex syntax injection issues.
String pattern1 = "\\b" + java.util.regex.Pattern.quote(smLower) + "\\b";
String pattern2 = "\\b" + java.util.regex.Pattern.quote(triggerLower) + "\\b";
// Treat underscores and hyphens as word boundary separators
String smNormalized = smLower.replace('_', ' ').replace('-', ' ');
String triggerNormalized = triggerLower.replace('_', ' ').replace('-', ' ');
return smNormalized.matches(".*" + pattern2 + ".*") || triggerNormalized.matches(".*" + pattern1 + ".*");
} }
private boolean isWildcardVariable(String eventStr) { private boolean isWildcardVariable(String eventStr) {

View File

@@ -1,7 +1,9 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing; package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
public interface BeanResolutionEngine { public interface BeanResolutionEngine {
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName); boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context);
} }

View File

@@ -1,6 +1,7 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing; package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -8,7 +9,35 @@ import java.util.Set;
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
@Override @Override
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) { public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
// Precise FQN Type argument match
if (chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
if (triggerEventFqn != null || triggerStateFqn != null) {
String[] machineTypes = getStateMachineTypeArguments(currentMachineName, context);
String machineStateFqn = machineTypes[0];
String machineEventFqn = machineTypes[1];
if (triggerEventFqn != null && machineEventFqn != null) {
if (triggerEventFqn.equals(machineEventFqn)) {
return true; // Match!
}
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
return false; // Mismatch!
}
}
if (triggerStateFqn != null && machineStateFqn != null) {
if (triggerStateFqn.equals(machineStateFqn)) {
return true; // Match!
}
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
return false; // Mismatch!
}
}
}
}
String targetVar = chain.getContextMachineId(); String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) { if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId(); targetVar = chain.getTriggerPoint().getStateMachineId();
@@ -119,37 +148,40 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) { private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false; if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
if (smPrefix == null || chainPrefix == null) return false;
String[] p1 = smPackage.split("\\."); String smLower = smPrefix.toLowerCase();
String[] p2 = chainPackage.split("\\."); String chainLower = chainPrefix.toLowerCase();
int matchIdx = -1; // Ignore generic/technical class prefixes
for (int i = 0; i < Math.min(p1.length, p2.length); i++) { if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") || smLower.equals("configuration") || smLower.equals("adapter") ||
if (p1[i].equals(p2[i])) { chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter")) {
matchIdx = i; return false;
} else { }
break;
// If class prefixes diverge, check if they are from different domains
if (!smLower.equals(chainLower)) {
// If one prefix is present as a domain/feature segment in the other package, they are related
if (chainPackage.toLowerCase().contains(smLower) || smPackage.toLowerCase().contains(chainLower)) {
return false;
}
// Otherwise, if they diverge under a shared project package root, it is a mismatch
String[] p1 = smPackage.split("\\.");
String[] p2 = chainPackage.split("\\.");
int matchIdx = -1;
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
if (p1[i].equals(p2[i])) {
matchIdx = i;
} else {
break;
}
}
if (matchIdx >= 1) {
return true; // Diverges under a shared root
} }
} }
Set<String> smDivergentTerms = new HashSet<>();
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
for (int i = matchIdx + 1; i < p1.length; i++) {
smDivergentTerms.add(p1[i].toLowerCase());
}
Set<String> chainDivergentTerms = new HashSet<>();
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
for (int i = matchIdx + 1; i < p2.length; i++) {
chainDivergentTerms.add(p2[i].toLowerCase());
}
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
Set<String> intersection = new HashSet<>(smDivergentTerms);
intersection.retainAll(chainDivergentTerms);
return intersection.isEmpty();
}
return false; return false;
} }
@@ -200,4 +232,97 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
} }
return null; return null;
} }
private String[] getStateMachineTypeArguments(String machineName, CodebaseContext context) {
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(machineName);
if (td != null) {
org.eclipse.jdt.core.dom.Type superclass = td.getSuperclassType();
Set<String> visited = new HashSet<>();
visited.add(machineName);
return findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
}
return new String[]{null, null};
}
private String[] findTypeArgumentsRecursively(org.eclipse.jdt.core.dom.Type type, CodebaseContext context, org.eclipse.jdt.core.dom.CompilationUnit cu, Set<String> visited) {
if (type == null) return new String[]{null, null};
if (type instanceof org.eclipse.jdt.core.dom.ParameterizedType pt) {
String rawTypeName = pt.getType().toString();
if (rawTypeName.equals("StateMachineConfigurerAdapter") || rawTypeName.equals("EnumStateMachineConfigurerAdapter") ||
rawTypeName.endsWith(".StateMachineConfigurerAdapter") || rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
java.util.List<?> typeArgs = pt.typeArguments();
if (typeArgs.size() >= 2) {
String stateType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(0), cu, context);
String eventType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(1), cu, context);
return new String[]{stateType, eventType};
}
}
// Recurse into the raw type declaration's superclass
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
if (td != null) {
String fqn = context.getFqn(td);
if (visited.add(fqn)) {
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
if (res[0] != null || res[1] != null) return res;
}
}
} else {
// It's a simple type (e.g. MyBaseConfig)
String simpleName = type.toString();
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
if (td != null) {
String fqn = context.getFqn(td);
if (visited.add(fqn)) {
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
if (res[0] != null || res[1] != null) return res;
}
}
}
return new String[]{null, null};
}
private String resolveTypeToFqn(org.eclipse.jdt.core.dom.Type type, org.eclipse.jdt.core.dom.CompilationUnit cu, CodebaseContext context) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((org.eclipse.jdt.core.dom.SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((org.eclipse.jdt.core.dom.ParameterizedType) type).getType(), cu, context);
} else {
simpleName = type.toString();
}
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
for (Object impObj : cu.imports()) {
org.eclipse.jdt.core.dom.ImportDeclaration imp = (org.eclipse.jdt.core.dom.ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
if (!packageName.isEmpty()) {
org.eclipse.jdt.core.dom.AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
if (localTd != null) return context.getFqn(localTd);
}
return simpleName;
}
private boolean isTypeMismatched(String type1, String type2) {
if (type1 == null || type2 == null) return false;
if (type1.equals("java.lang.String") || type1.equals("String") || type1.equals("java.lang.Object") || type1.equals("Object")) return false;
if (type2.equals("java.lang.String") || type2.equals("String") || type2.equals("java.lang.Object") || type2.equals("Object")) return false;
return !type1.equals(type2);
}
} }

View File

@@ -20,5 +20,9 @@ public class TriggerPoint {
private final String stateMachineId; // Optional: to link to a specific SM instance private final String stateMachineId; // Optional: to link to a specific SM instance
private final String sourceState; // Optional: if we can determine the expected current state private final String sourceState; // Optional: if we can determine the expected current state
private final int lineNumber; private final int lineNumber;
@com.fasterxml.jackson.annotation.JsonIgnore
private final String stateTypeFqn; // Type of State (e.g. OrderStates FQN)
@com.fasterxml.jackson.annotation.JsonIgnore
private final String eventTypeFqn; // Type of Event (e.g. OrderEvents FQN)
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
} }

View File

@@ -2,10 +2,12 @@ package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*; import org.eclipse.jdt.core.dom.*;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
@Slf4j
public class ConstantExtractor { public class ConstantExtractor {
private final CodebaseContext context; private final CodebaseContext context;
private final ConstantResolver constantResolver; private final ConstantResolver constantResolver;
@@ -132,57 +134,62 @@ public class ConstantExtractor {
} }
} }
if (mi.getExpression() instanceof ClassInstanceCreation cic) { List<Expression> receivers = new ArrayList<>();
if (cic.getAnonymousClassDeclaration() != null) { if (mi.getExpression() != null) {
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { if (mi.getExpression() instanceof ClassInstanceCreation) {
if (declObj instanceof MethodDeclaration mdecl) { receivers.add(mi.getExpression());
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) { } else if (variableTracer != null) {
mdecl.getBody().accept(new ASTVisitor() { receivers.addAll(variableTracer.traceVariableAll(mi.getExpression()));
@Override }
public boolean visit(ReturnStatement rs) { }
if (rs.getExpression() != null) {
extractConstantsFromExpression(rs.getExpression(), constants); for (Expression receiverExpr : receivers) {
if (receiverExpr instanceof ClassInstanceCreation cic) {
if (cic.getAnonymousClassDeclaration() != null) {
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
if (declObj instanceof MethodDeclaration mdecl) {
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) {
mdecl.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement rs) {
if (rs.getExpression() != null) {
extractConstantsFromExpression(rs.getExpression(), constants);
}
return super.visit(rs);
} }
return super.visit(rs); });
} }
});
} }
} }
} } else if (constructorAnalyzer != null) {
} else if (constructorAnalyzer != null) { boolean handledByLombok = false;
boolean handledByLombok = false; if (methodName.startsWith("get") && methodName.length() > 3) {
if (methodName.startsWith("get") && methodName.length() > 3) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
if (typeDecl != null) {
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
int fieldIndex = constructorAnalyzer.findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
extractConstantsFromArgument((Expression) cic.arguments().get(fieldIndex), constants);
handledByLombok = true;
}
}
}
if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
List<String> narrowed = constructorAnalyzer.resolveInlineInstantiationGetterResult(
cic, methodName, context, new HashSet<>());
if (narrowed.isEmpty()) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null; TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName);
if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName);
if (typeDecl != null) { if (typeDecl != null) {
narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new HashSet<>(), contextCu); String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
int fieldIndex = constructorAnalyzer.findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
extractConstantsFromArgument((Expression) cic.arguments().get(fieldIndex), constants);
handledByLombok = true;
}
} }
} }
if (narrowed != null && !narrowed.isEmpty()) { if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
constants.addAll(narrowed); List<String> narrowed = constructorAnalyzer.resolveInlineInstantiationGetterResult(
handledByLombok = true; cic, methodName, context, new HashSet<>());
} if (narrowed.isEmpty()) {
} String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
if (!handledByLombok) { CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null;
for (Object argObj : cic.arguments()) { TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName);
extractConstantsFromArgument((Expression) argObj, constants); if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName);
if (typeDecl != null) {
narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new HashSet<>(), contextCu);
}
}
if (narrowed != null && !narrowed.isEmpty()) {
constants.addAll(narrowed);
}
} }
} }
} }
@@ -258,9 +265,16 @@ public class ConstantExtractor {
} }
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) { public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
if (depth > 20) return Collections.emptyList(); log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
if (depth > 20) {
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
return Collections.emptyList();
}
String fqn = className + "." + methodName; String fqn = className + "." + methodName;
if (!visited.add(fqn)) return Collections.emptyList(); if (!visited.add(fqn)) {
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
return Collections.emptyList();
}
List<String> constants = new ArrayList<>(); List<String> constants = new ArrayList<>();
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null; TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
@@ -358,6 +372,7 @@ public class ConstantExtractor {
} }
} }
visited.remove(fqn); visited.remove(fqn);
log.debug("resolveMethodReturnConstant for class={}, method={} resolved constants: {}", className, methodName, constants);
return constants; return constants;
} }

View File

@@ -30,6 +30,15 @@ public class ConstructorAnalyzer {
} }
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) { public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
if (td == null || fieldName == null) return Collections.emptyList();
String fqn = context.getFqn(td);
if (fqn == null) return Collections.emptyList();
String cycleKey = fqn + "#" + fieldName;
if (!visited.add(cycleKey)) {
return Collections.emptyList();
}
List<String> results = new ArrayList<>(); List<String> results = new ArrayList<>();
for (FieldDeclaration fd : td.getFields()) { for (FieldDeclaration fd : td.getFields()) {
@@ -136,6 +145,49 @@ public class ConstructorAnalyzer {
} }
} }
if (results.isEmpty()) {
// Context-Aware Fallback (Up): Check superclass constructors
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited));
}
}
}
if (results.isEmpty()) {
// Context-Aware Fallback (Down): Check subclasses/implementations
List<String> impls = context.getImplementations(fqn);
if (impls != null) {
for (String implFqn : impls) {
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
if (implTd != null) {
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited));
}
}
}
}
if (results.isEmpty()) {
// Global Fallback: Search all parsed classes in the workspace
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
if (allTypes != null) {
int matchedCount = 0;
for (TypeDeclaration typeDecl : allTypes) {
if (typeDecl == null) continue;
if (context.hasField(typeDecl, fieldName)) {
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited));
matchedCount++;
if (matchedCount >= 10) {
break;
}
}
}
}
}
visited.remove(cycleKey);
return results; return results;
} }
@@ -473,9 +525,47 @@ public class ConstructorAnalyzer {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null; CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null;
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); AbstractTypeDeclaration atd = contextCu != null ? context.getAbstractTypeDeclaration(typeName, contextCu) : context.getAbstractTypeDeclaration(typeName);
if (td == null) td = context.getTypeDeclaration(typeName); if (atd == null) atd = context.getAbstractTypeDeclaration(typeName);
if (td == null) return Collections.emptyList(); if (atd == null) return Collections.emptyList();
if (atd instanceof RecordDeclaration rd) {
int compIdx = -1;
List<?> components = rd.recordComponents();
for (int i = 0; i < components.size(); i++) {
Object compObj = components.get(i);
if (compObj != null) {
try {
java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
SimpleName nameObj = (SimpleName) getNameMethod.invoke(compObj);
if (nameObj != null && nameObj.getIdentifier().equals(getterName)) {
compIdx = i;
break;
}
} catch (Exception e) {
// ignore reflection errors
}
}
}
if (compIdx >= 0 && compIdx < cic.arguments().size()) {
Expression argExpr = (Expression) cic.arguments().get(compIdx);
List<String> consts = new ArrayList<>();
if (constantExtractor != null) {
constantExtractor.extractConstantsFromArgument(argExpr, consts);
}
if (!consts.isEmpty()) {
return consts;
} else {
String resolved = constantResolver.resolve(argExpr, context);
if (resolved != null) {
return List.of(resolved);
}
}
}
return Collections.emptyList();
}
if (!(atd instanceof TypeDeclaration td)) return Collections.emptyList();
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true); MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
if (getter == null || getter.getBody() == null) return Collections.emptyList(); if (getter == null || getter.getBody() == null) return Collections.emptyList();

View File

@@ -141,6 +141,7 @@ public class GenericEventDetector {
if (type == null) return Collections.emptyList(); if (type == null) return Collections.emptyList();
String sourceState = extractSourceState(node); String sourceState = extractSourceState(node);
String[] smTypes = resolveStateMachineTypeArguments(node);
List<TriggerPoint> results = new ArrayList<>(); List<TriggerPoint> results = new ArrayList<>();
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) { if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
@@ -154,6 +155,8 @@ public class GenericEventDetector {
.methodName(method != null ? method.getName().getIdentifier() : "initializer") .methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type))) .sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition())) .lineNumber(cu.getLineNumber(node.getStartPosition()))
.stateTypeFqn(smTypes[0])
.eventTypeFqn(smTypes[1])
.build()); .build());
} }
} }
@@ -165,6 +168,8 @@ public class GenericEventDetector {
.methodName(method != null ? method.getName().getIdentifier() : "initializer") .methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type))) .sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition())) .lineNumber(cu.getLineNumber(node.getStartPosition()))
.stateTypeFqn(smTypes[0])
.eventTypeFqn(smTypes[1])
.build()); .build());
} }
@@ -410,4 +415,139 @@ public class GenericEventDetector {
} }
return (TypeDeclaration) parent; return (TypeDeclaration) parent;
} }
private String[] resolveStateMachineTypeArguments(MethodInvocation node) {
Expression receiver = node.getExpression();
if (receiver == null) return new String[]{null, null};
ITypeBinding binding = receiver.resolveTypeBinding();
if (binding != null) {
ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
if (smBinding != null) {
ITypeBinding[] typeArgs = smBinding.getTypeArguments();
if (typeArgs.length >= 2) {
return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()};
}
}
}
// Fallback: search enclosing class/method for variable declaration
Type declType = findReceiverTypeManually(receiver, node);
if (declType instanceof ParameterizedType pt) {
List<?> typeArgs = pt.typeArguments();
if (typeArgs.size() >= 2) {
CompilationUnit cu = (CompilationUnit) node.getRoot();
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu);
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu);
return new String[]{stateType, eventType};
}
}
return new String[]{null, null};
}
private ITypeBinding findStateMachineSupertype(ITypeBinding binding, java.util.Set<String> visited) {
ITypeBinding current = binding;
while (current != null) {
String erasureName = current.getErasure().getQualifiedName();
if (erasureName != null && !visited.add(erasureName)) {
return null;
}
if ("org.springframework.statemachine.StateMachine".equals(erasureName)) {
return current;
}
for (ITypeBinding iface : current.getInterfaces()) {
ITypeBinding res = findStateMachineSupertype(iface, visited);
if (res != null) return res;
}
current = current.getSuperclass();
}
return null;
}
private Type findReceiverTypeManually(Expression receiver, MethodInvocation node) {
if (!(receiver instanceof SimpleName sn)) return null;
String varName = sn.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(node);
if (enclosingMethod != null) {
// Check params
for (Object paramObj : enclosingMethod.parameters()) {
if (paramObj instanceof SingleVariableDeclaration svd) {
if (svd.getName().getIdentifier().equals(varName)) {
return svd.getType();
}
}
}
// Check local variables
if (enclosingMethod.getBody() != null) {
Type[] found = new Type[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement vds) {
for (Object fragObj : vds.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(varName)) {
found[0] = vds.getType();
}
}
}
return super.visit(vds);
}
});
if (found[0] != null) return found[0];
}
}
// Check fields
TypeDeclaration enclosingType = findEnclosingType(node);
if (enclosingType != null) {
for (FieldDeclaration fd : enclosingType.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(varName)) {
return fd.getType();
}
}
}
}
}
return null;
}
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu);
} else {
simpleName = type.toString();
}
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
// Check package name
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
if (!packageName.isEmpty()) {
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
if (localTd != null) return context.getFqn(localTd);
}
return simpleName;
}
} }

View File

@@ -88,4 +88,22 @@ class HeuristicEventMatchingEngineTest {
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build(); TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build();
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue(); assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
} }
@Test
void shouldMatchCaseInsensitive() {
Event smEvent = Event.of("PAY", "PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("pay").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchGuardedContains() {
Event smEvent = Event.of("PAY", "PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("pay_order").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
Event smEvent2 = Event.of("PAY_ORDER", "PAY_ORDER");
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("pay").build();
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
}
} }

View File

@@ -22,7 +22,7 @@ public class HeuristicBeanResolutionEngineTest {
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine"; String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName); boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// They share com.acme.corp.division.project, but diverge into orders vs payments // They share com.acme.corp.division.project, but diverge into orders vs payments
// This should be a strong mismatch, so it should return false // This should be a strong mismatch, so it should return false
@@ -37,7 +37,7 @@ public class HeuristicBeanResolutionEngineTest {
String machineName = "com.acme.ecommerce.orders.OrderStateMachine"; String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName); boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// Same exact domain, should match // Same exact domain, should match
assertTrue(result, "Same domain should be accepted"); assertTrue(result, "Same domain should be accepted");
@@ -51,7 +51,7 @@ public class HeuristicBeanResolutionEngineTest {
String machineName = "com.acme.ecommerce.orders.OrderStateMachine"; String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName); boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// Subpackage should match // Subpackage should match
assertTrue(result, "Sub-packages of the same domain should be accepted"); assertTrue(result, "Sub-packages of the same domain should be accepted");
@@ -65,7 +65,7 @@ public class HeuristicBeanResolutionEngineTest {
String machineName = "com.acme.orders.service.OrderStateMachine"; String machineName = "com.acme.orders.service.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName); boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// Different subpackages, but they share the "order" domain term and common prefix // Different subpackages, but they share the "order" domain term and common prefix
assertTrue(result, "Divergent packages that share a domain term should be accepted"); assertTrue(result, "Divergent packages that share a domain term should be accepted");
@@ -80,7 +80,7 @@ public class HeuristicBeanResolutionEngineTest {
String machineName = "com.acme.corp.payments.PaymentStateMachine"; String machineName = "com.acme.corp.payments.PaymentStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName); boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// Mismatched domains, but explicit variable targeting works // Mismatched domains, but explicit variable targeting works
assertTrue(result, "Explicit variable target matching the machine name should be accepted"); assertTrue(result, "Explicit variable target matching the machine name should be accepted");
@@ -95,9 +95,24 @@ public class HeuristicBeanResolutionEngineTest {
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
boolean result = engine.isRoutedToCorrectMachine(chain, machineName); boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// Explicit variable targeting completely conflicts // Explicit variable targeting completely conflicts
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected"); assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
} }
@Test
public void testGenericStateMachineConfigInGenericPackage() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.corp.orders.OrderController.submit()"))
.build();
String machineName = "com.acme.corp.config.StateMachineConfig";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
// StateMachineConfig has generic prefix and is in config package. OrderController is in orders.
// There should be no domain mismatch because config/statemachine are generic.
assertTrue(result, "Generic state machine config in generic config package should not trigger mismatch");
}
} }

View File

@@ -99,7 +99,7 @@ class EnterpriseBugsTest {
.build(); .build();
// Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted // Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted
boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine"); boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine", null);
assertThat(isMatched).isTrue(); assertThat(isMatched).isTrue();
} }
@@ -120,4 +120,52 @@ class EnterpriseBugsTest {
// Assert Bug 6: resolves cleanly to simple name (since prefix is a known type) // Assert Bug 6: resolves cleanly to simple name (since prefix is a known type)
assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp"); assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp");
} }
@Test
void shouldMatchFqnsAndMismatchesOnRouting(@TempDir Path tempDir) throws IOException {
String statesSrc = """
package com.example;
public enum OrderStates { S1, S2 }
""";
String eventsSrc = """
package com.example;
public enum OrderEvents { E1, E2 }
""";
String otherEventsSrc = """
package com.example.other;
public enum OtherEvents { E1, E2 }
""";
String configSrc = """
package com.example;
public class OrderStateMachineConfig extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderStates, OrderEvents> {}
""";
Files.writeString(tempDir.resolve("OrderStates.java"), statesSrc);
Files.writeString(tempDir.resolve("OrderEvents.java"), eventsSrc);
Files.writeString(tempDir.resolve("OtherEvents.java"), otherEventsSrc);
Files.writeString(tempDir.resolve("OrderStateMachineConfig.java"), configSrc);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
// Scenario 1: Exact Event type argument match
CallChain chain1 = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.eventTypeFqn("com.example.OrderEvents")
.className("com.example.Service")
.build())
.build();
assertThat(engine.isRoutedToCorrectMachine(chain1, "com.example.OrderStateMachineConfig", context)).isTrue();
// Scenario 2: Event type argument mismatch
CallChain chain2 = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.eventTypeFqn("com.example.other.OtherEvents")
.className("com.example.Service")
.build())
.build();
assertThat(engine.isRoutedToCorrectMachine(chain2, "com.example.OrderStateMachineConfig", context)).isFalse();
}
} }