update argument resolution to

This commit is contained in:
2026-06-28 07:02:36 +02:00
parent ff2c8f8cfb
commit 93688ef59b
9 changed files with 412 additions and 46 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,41 +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;
} else {
break;
}
}
if (matchIdx < 1) {
return false; return false;
} }
Set<String> smDivergentTerms = new HashSet<>(); // If class prefixes diverge, check if they are from different domains
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase()); if (!smLower.equals(chainLower)) {
for (int i = matchIdx + 1; i < p1.length; i++) { // If one prefix is present as a domain/feature segment in the other package, they are related
smDivergentTerms.add(p1[i].toLowerCase()); 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> 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;
} }
@@ -204,4 +232,87 @@ 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();
return findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
}
return new String[]{null, null};
}
private String[] findTypeArgumentsRecursively(org.eclipse.jdt.core.dom.Type type, CodebaseContext context, org.eclipse.jdt.core.dom.CompilationUnit cu) {
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.TypeDeclaration td = context.getTypeDeclaration(rawTypeName, cu);
if (td != null) {
String[] res = findTypeArgumentsRecursively(td.getSuperclassType(), context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
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.TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
String[] res = findTypeArgumentsRecursively(td.getSuperclassType(), context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
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

@@ -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,135 @@ 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);
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) {
ITypeBinding current = binding;
while (current != null) {
if (current.getErasure().getQualifiedName().equals("org.springframework.statemachine.StateMachine")) {
return current;
}
for (ITypeBinding iface : current.getInterfaces()) {
ITypeBinding res = findStateMachineSupertype(iface);
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();
}
} }