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 3a6dfc2..4e8b1b3 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 @@ -98,7 +98,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { hasPositiveMatch = true; } - if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix)) { + if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix, smSimple, chainSimple)) { hasStrongMismatch = true; } } @@ -146,7 +146,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { return false; } - private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) { + private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix, String smSimple, String chainSimple) { if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false; if (smPrefix == null || chainPrefix == null) return false; @@ -155,7 +155,9 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { // 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")) { + smLower.equals("enterprise") || smLower.equals("app") || smLower.equals("global") || smLower.equals("core") || smLower.equals("project") || smLower.equals("main") || smLower.equals("base") || smLower.equals("common") || smLower.equals("shared") || + chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter") || + chainLower.equals("enterprise") || chainLower.equals("app") || chainLower.equals("global") || chainLower.equals("core") || chainLower.equals("project") || chainLower.equals("main") || chainLower.equals("base") || chainLower.equals("common") || chainLower.equals("shared")) { return false; } @@ -178,7 +180,33 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { } } if (matchIdx >= 1) { - return true; // Diverges under a shared root + // If they diverge under a shared root, check if the divergence is only on generic package layers + boolean onlyGenericDivergence = true; + Set genericLayers = Set.of( + "config", "configuration", "service", "services", "impl", "api", "web", + "controller", "controllers", "messaging", "listener", "listeners", + "repository", "repositories", "db", "model", "models", "domain", + "constants", "utils", "helper", "helpers", "event", "events", + "state", "states", "transition", "transitions" + ); + for (int i = matchIdx + 1; i < p1.length; i++) { + if (!genericLayers.contains(p1[i].toLowerCase())) { + onlyGenericDivergence = false; + break; + } + } + if (onlyGenericDivergence) { + for (int i = matchIdx + 1; i < p2.length; i++) { + if (!genericLayers.contains(p2[i].toLowerCase())) { + onlyGenericDivergence = false; + break; + } + } + } + + if (!onlyGenericDivergence) { + return true; // Mismatch under a shared root + } } } @@ -325,4 +353,12 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { if (type2.equals("java.lang.String") || type2.equals("String") || type2.equals("java.lang.Object") || type2.equals("Object")) return false; return !type1.equals(type2); } + + private boolean isSingleStateMachine(CodebaseContext context) { + if (context == null) return false; + int count = 0; + count += context.findEntryPointClasses(java.util.List.of("EnableStateMachineFactory", "EnableStateMachine")).size(); + count += context.findBeanMethodsReturning(java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size(); + return count <= 1; + } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index fcce364..fea6a3d 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -822,7 +822,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { String varName = receiverName; Expression initExpr = findVariableInitializer(entryMethod, varName); if (initExpr instanceof MethodInvocation initMi) { - initExpr = unwrapMethodInvocation(initMi, 0); + initExpr = unwrapMethodInvocation(initMi, 0, entryMethod); } if (!(initExpr instanceof ClassInstanceCreation cic)) { return null; @@ -922,7 +922,40 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return new String[] { paramName, currentSuffix }; } + private int getReturnedParameterIndex(MethodDeclaration md) { + if (md.getBody() == null) return -1; + List parameters = md.parameters(); + if (parameters.isEmpty()) return -1; + + final List returnedExprs = new ArrayList<>(); + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + if (node.getExpression() != null) { + returnedExprs.add(node.getExpression().toString()); + } + return super.visit(node); + } + }); + + if (returnedExprs.size() == 1) { + String retStr = returnedExprs.get(0); + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) instanceof SingleVariableDeclaration svd) { + if (svd.getName().getIdentifier().equals(retStr)) { + return i; + } + } + } + } + return -1; + } + protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) { + return unwrapMethodInvocation(mi, depth, null); + } + + protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) { if (depth > 5) return mi; if (mi.getExpression() != null) { String exprStr = mi.getExpression().toString(); @@ -930,6 +963,23 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return mi; } } else { + if (methodFqn != null && methodFqn.contains(".")) { + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + MethodDeclaration localMd = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true); + if (localMd != null) { + int paramIdx = getReturnedParameterIndex(localMd); + if (paramIdx >= 0 && paramIdx < mi.arguments().size()) { + Expression arg = (Expression) mi.arguments().get(paramIdx); + if (arg instanceof MethodInvocation innerMi) { + return unwrapMethodInvocation(innerMi, depth + 1, methodFqn); + } + return arg; + } + } + } + } return mi; } @@ -951,7 +1001,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { Expression arg = (Expression) mi.arguments().get(0); if (arg instanceof MethodInvocation innerMi) { - return unwrapMethodInvocation(innerMi, depth + 1); + return unwrapMethodInvocation(innerMi, depth + 1, methodFqn); } return arg; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index 5ef2fe1..0aab6e2 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -282,10 +282,11 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } } - // 2. Check fields in enclosing class + // 2. Check fields in enclosing class and its superclasses TypeDeclaration enclosingType = findEnclosingType(receiverNameNode); - if (enclosingType != null) { - for (FieldDeclaration field : enclosingType.getFields()) { + TypeDeclaration currentType = enclosingType; + while (currentType != null) { + for (FieldDeclaration field : currentType.getFields()) { for (Object fragObj : field.fragments()) { VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; if (frag.getName().getIdentifier().equals(varName)) { @@ -293,6 +294,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } } } + String superclass = context.getSuperclassFqn(currentType); + currentType = superclass != null ? context.getTypeDeclaration(superclass) : null; } return null; @@ -604,6 +607,9 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } if (receiverType != null) { + if (receiverType.contains("<")) { + receiverType = receiverType.substring(0, receiverType.indexOf('<')); + } String methodName = mi.getName().getIdentifier(); TypeDeclaration td = context.getTypeDeclaration(receiverType); if (td == null && receiverType.contains(".")) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index b306c4b..36106a3 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -288,10 +288,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { } } - // 2. Check fields in enclosing class + // 2. Check fields in enclosing class and its superclasses TypeDeclaration enclosingType = findEnclosingType(receiverNameNode); - if (enclosingType != null) { - for (FieldDeclaration field : enclosingType.getFields()) { + TypeDeclaration currentType = enclosingType; + while (currentType != null) { + for (FieldDeclaration field : currentType.getFields()) { for (Object fragObj : field.fragments()) { VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; if (frag.getName().getIdentifier().equals(varName)) { @@ -299,6 +300,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { } } } + String superclass = context.getSuperclassFqn(currentType); + currentType = superclass != null ? context.getTypeDeclaration(superclass) : null; } return null; @@ -376,6 +379,9 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { } if (receiverType != null) { + if (receiverType.contains("<")) { + receiverType = receiverType.substring(0, receiverType.indexOf('<')); + } String methodName = mi.getName().getIdentifier(); TypeDeclaration td = context.getTypeDeclaration(receiverType); if (td == null && receiverType.contains(".")) { 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 8d42b7a..2459f4c 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 @@ -115,4 +115,16 @@ public class HeuristicBeanResolutionEngineTest { // 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"); } + + @Test + public void testNoMismatchOnStateMachineConfigSuffix() { + CallChain chain = CallChain.builder() + .methodChain(Arrays.asList("com.acme.shipping.service.OrderServiceImpl.place()")) + .build(); + + String machineName = "com.acme.shipping.config.MyStateMachineConfig"; + + boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null); + assertTrue(result, "MyStateMachineConfig should not trigger mismatch because it contains StateMachineConfig"); + } }