diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index 179a2a7..6da6afb 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -59,7 +59,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { .targetState(smSourceRaw) .event(smEventRaw) .build(); - if (isRoutedToCorrectMachine(chain, result.getName())) { + if (isRoutedToCorrectMachine(chain, result.getName(), context)) { matched.add(mt); } } else { @@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { .targetState(targetRaw) .event(smEventRaw) .build(); - if (isRoutedToCorrectMachine(chain, result.getName())) { + if (isRoutedToCorrectMachine(chain, result.getName(), context)) { matched.add(mt); } } @@ -100,8 +100,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { } - private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) { - return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName); + private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) { + return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context); } private String simplify(String name) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java index c725591..c8e3cdf 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java @@ -37,7 +37,8 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { String simplifiedPe = simplify(simplePe); if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) || - simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) { + simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) || + isGuardedContains(smEvent, simplifiedPe)) { hasPolyMatch = true; break; } @@ -54,7 +55,38 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { } 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) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/BeanResolutionEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/BeanResolutionEngine.java index abe516f..7812373 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/BeanResolutionEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/BeanResolutionEngine.java @@ -1,7 +1,9 @@ package click.kamil.springstatemachineexporter.analysis.enricher.routing; import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; public interface BeanResolutionEngine { - boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName); + boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context); } + diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java index 257dbb5..8b80032 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java @@ -1,6 +1,7 @@ package click.kamil.springstatemachineexporter.analysis.enricher.routing; import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import java.util.HashSet; import java.util.Set; @@ -8,7 +9,35 @@ import java.util.Set; public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { @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(); if (targetVar == null && chain.getTriggerPoint() != null) { targetVar = chain.getTriggerPoint().getStateMachineId(); @@ -119,41 +148,40 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) { if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false; + if (smPrefix == null || chainPrefix == null) return false; - 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) { + String smLower = smPrefix.toLowerCase(); + String chainLower = chainPrefix.toLowerCase(); + + // Ignore generic/technical class prefixes + if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") || smLower.equals("configuration") || smLower.equals("adapter") || + chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter")) { return false; } - - Set smDivergentTerms = new HashSet<>(); - if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase()); - for (int i = matchIdx + 1; i < p1.length; i++) { - smDivergentTerms.add(p1[i].toLowerCase()); + + // 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 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 intersection = new HashSet<>(smDivergentTerms); - intersection.retainAll(chainDivergentTerms); - return intersection.isEmpty(); - } - + return false; } @@ -204,4 +232,87 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { } 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); + } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java index c6ec1b9..c6fc62f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java @@ -20,5 +20,9 @@ public class TriggerPoint { 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 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 polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java index a9a67dc..89c565b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java @@ -141,6 +141,7 @@ public class GenericEventDetector { if (type == null) return Collections.emptyList(); String sourceState = extractSourceState(node); + String[] smTypes = resolveStateMachineTypeArguments(node); List results = new ArrayList<>(); if (eventValue != null && eventValue.startsWith("ENUM_SET:")) { @@ -154,6 +155,8 @@ public class GenericEventDetector { .methodName(method != null ? method.getName().getIdentifier() : "initializer") .sourceFile(context.getRelativePath(context.getFqn(type))) .lineNumber(cu.getLineNumber(node.getStartPosition())) + .stateTypeFqn(smTypes[0]) + .eventTypeFqn(smTypes[1]) .build()); } } @@ -165,6 +168,8 @@ public class GenericEventDetector { .methodName(method != null ? method.getName().getIdentifier() : "initializer") .sourceFile(context.getRelativePath(context.getFqn(type))) .lineNumber(cu.getLineNumber(node.getStartPosition())) + .stateTypeFqn(smTypes[0]) + .eventTypeFqn(smTypes[1]) .build()); } @@ -410,4 +415,135 @@ public class GenericEventDetector { } 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; + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java index c7805b9..c272979 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngineTest.java @@ -88,4 +88,22 @@ class HeuristicEventMatchingEngineTest { TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build(); 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(); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngineTest.java index 3ef06fb..8d42b7a 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngineTest.java @@ -22,7 +22,7 @@ public class HeuristicBeanResolutionEngineTest { 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 // 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"; - boolean result = engine.isRoutedToCorrectMachine(chain, machineName); + boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null); // Same exact domain, should match assertTrue(result, "Same domain should be accepted"); @@ -51,7 +51,7 @@ public class HeuristicBeanResolutionEngineTest { String machineName = "com.acme.ecommerce.orders.OrderStateMachine"; - boolean result = engine.isRoutedToCorrectMachine(chain, machineName); + boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null); // Subpackage should match 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"; - 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 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"; - boolean result = engine.isRoutedToCorrectMachine(chain, machineName); + boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null); // Mismatched domains, but explicit variable targeting works 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 - boolean result = engine.isRoutedToCorrectMachine(chain, machineName); + boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null); // Explicit variable targeting completely conflicts 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"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java index e90caa1..74819d2 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java @@ -99,7 +99,7 @@ class EnterpriseBugsTest { .build(); // 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(); } @@ -120,4 +120,52 @@ class EnterpriseBugsTest { // Assert Bug 6: resolves cleanly to simple name (since prefix is a known type) 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 {} + """; + + 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(); + } }