From ff16c1bbe7241af03de922122ee9ee260dccc062 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 20 Jun 2026 13:50:53 +0200 Subject: [PATCH] fix 6 --- .../enricher/TransitionLinkerEnricher.java | 39 +++- .../analysis/resolver/BeanDefinition.java | 18 ++ .../analysis/resolver/BeanRegistry.java | 84 +++++++++ .../analysis/resolver/BeanScanner.java | 171 ++++++++++++++++++ .../analysis/service/CallGraphBuilder.java | 102 ++++++++++- .../ast/common/CodebaseContext.java | 33 +++- .../GoldenUpdater.java | 52 +++++- .../analysis/resolver/BeanRegistryTest.java | 123 +++++++++++++ .../service/CallGraphBuilderTest.java | 70 ++++++- .../MavenOrderStateMachine.png | Bin 0 -> 9017 bytes .../OrderStateMachineConfig.png | Bin 0 -> 10017 bytes .../StateMachineConfig/StateMachineConfig.png | Bin 0 -> 22595 bytes 12 files changed, 670 insertions(+), 22 deletions(-) create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanDefinition.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistry.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanScanner.java create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistryTest.java create mode 100644 state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.png create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.png create mode 100644 state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.png 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 1163428..0440498 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 @@ -239,12 +239,20 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { return null; } + private String getDomainRootPackage(String[] p) { + if (p.length > 0 && (p[p.length - 1].equals("config") || p[p.length - 1].equals("configuration"))) { + return String.join(".", java.util.Arrays.copyOf(p, p.length - 1)); + } + return String.join(".", p); + } + private String getDeepSharedPackage(String pkg1, String pkg2) { if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null; - String[] p1 = pkg1.split("\\."); + + String domain1 = getDomainRootPackage(pkg1.split("\\.")); + String[] p1 = domain1.split("\\."); String[] p2 = pkg2.split("\\."); - // Find the deepest common segment index int matchIdx = -1; for (int i = 0; i < Math.min(p1.length, p2.length); i++) { if (p1[i].equals(p2[i])) { @@ -253,8 +261,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { break; } } - // If they share at least 3 segments (e.g. com.company.electronics), it's a deep match - if (matchIdx >= 2) { + + // Require matching up to the domain root + int requiredMatchIdx = Math.max(1, p1.length - 1); + if (matchIdx >= requiredMatchIdx) { StringBuilder sb = new StringBuilder(); for(int i = 0; i <= matchIdx; i++) { sb.append(p1[i]).append("."); @@ -266,13 +276,24 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { private boolean isDeepPackageDivergence(String pkg1, String pkg2) { if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false; - String[] p1 = pkg1.split("\\."); + + String domain1 = getDomainRootPackage(pkg1.split("\\.")); + String[] p1 = domain1.split("\\."); String[] p2 = pkg2.split("\\."); - // If they share exactly the root (com.company) but diverge at the 3rd segment (e.g. electronics vs computerstore) - // Then it's a divergence. - if (p1.length >= 3 && p2.length >= 3) { - return p1[0].equals(p2[0]) && p1[1].equals(p2[1]) && !p1[2].equals(p2[2]); + 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; + } + } + + // Dynamic threshold based on domain root + int requiredBaseMatchIdx = p1.length - 2; + if (requiredBaseMatchIdx >= 1 && matchIdx == requiredBaseMatchIdx) { + return p2.length > matchIdx + 1; } return false; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanDefinition.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanDefinition.java new file mode 100644 index 0000000..e75a80a --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanDefinition.java @@ -0,0 +1,18 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import lombok.Builder; +import lombok.Data; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import java.util.List; + +@Data +@Builder +public class BeanDefinition { + private String name; + private String typeFqn; + private List qualifiers; + private boolean isPrimary; + private TypeDeclaration declaringClass; + private MethodDeclaration declaringMethod; +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistry.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistry.java new file mode 100644 index 0000000..b5f5bdd --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistry.java @@ -0,0 +1,84 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.RequiredArgsConstructor; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class BeanRegistry { + private final CodebaseContext context; + private final List beans = new ArrayList<>(); + + public void register(BeanDefinition bean) { + beans.add(bean); + } + + public List getBeans() { + return beans; + } + + public BeanDefinition resolve(String expectedTypeFqn, String expectedName, String expectedQualifier) { + System.out.println("RESOLVING: type=" + expectedTypeFqn + " name=" + expectedName + " qual=" + expectedQualifier); + // 1. Qualifier Match + if (expectedQualifier != null && !expectedQualifier.isEmpty()) { + for (BeanDefinition bean : beans) { + if (matchesType(bean.getTypeFqn(), expectedTypeFqn) && bean.getQualifiers().contains(expectedQualifier)) { + System.out.println(" -> Matched by qualifier: " + bean.getName()); + return bean; + } + } + } + + List typeMatches = new ArrayList<>(); + for (BeanDefinition bean : beans) { + System.out.println(" Checking bean: " + bean.getName() + " of type " + bean.getTypeFqn()); + if (matchesType(bean.getTypeFqn(), expectedTypeFqn)) { + typeMatches.add(bean); + } + } + + if (typeMatches.isEmpty()) { + System.out.println(" -> No type matches found"); + return null; + } + + if (typeMatches.size() == 1) { + System.out.println(" -> Single type match: " + typeMatches.get(0).getName()); + return typeMatches.get(0); + } + + // 2. Name Match + if (expectedName != null && !expectedName.isEmpty()) { + for (BeanDefinition bean : typeMatches) { + if (expectedName.equals(bean.getName())) { + System.out.println(" -> Matched by name: " + bean.getName()); + return bean; + } + } + } + + // 3. Primary Match + for (BeanDefinition bean : typeMatches) { + if (bean.isPrimary()) { + System.out.println(" -> Matched by primary: " + bean.getName()); + return bean; + } + } + + // 4. Fallback + System.out.println(" -> Fallback (first match): " + typeMatches.get(0).getName()); + return typeMatches.get(0); + } + + private boolean matchesType(String actualTypeFqn, String expectedTypeFqn) { + if (actualTypeFqn == null || expectedTypeFqn == null) return false; + if (actualTypeFqn.equals(expectedTypeFqn)) return true; + + // Check if actualTypeFqn implements expectedTypeFqn + List actualImpls = context.getImplementations(expectedTypeFqn); + return actualImpls.contains(actualTypeFqn); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanScanner.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanScanner.java new file mode 100644 index 0000000..5abb11b --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanScanner.java @@ -0,0 +1,171 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.List; + +public class BeanScanner { + + public static void scanAndRegister(CompilationUnit cu, CodebaseContext context, BeanRegistry registry) { + cu.accept(new ASTVisitor() { + @Override + public boolean visit(TypeDeclaration node) { + boolean isComponent = false; + boolean isPrimary = false; + String beanName = null; + List qualifiers = new ArrayList<>(); + + for (Object modifier : node.modifiers()) { + if (modifier instanceof Annotation annotation) { + String typeName = annotation.getTypeName().getFullyQualifiedName(); + if (typeName.endsWith("Component") || typeName.endsWith("Service") || + typeName.endsWith("Repository") || typeName.endsWith("Controller") || + typeName.endsWith("Configuration")) { + isComponent = true; + String value = extractAnnotationValue(annotation, "value"); + if (!value.isEmpty()) { + beanName = value.replace("\"", ""); + } + } else if (typeName.endsWith("Primary")) { + isPrimary = true; + } else if (typeName.endsWith("Qualifier")) { + String value = extractAnnotationValue(annotation, "value"); + if (!value.isEmpty()) { + qualifiers.add(value.replace("\"", "")); + } + } + } + } + + if (isComponent) { + if (beanName == null) { + beanName = decapitalize(node.getName().getIdentifier()); + } + registry.register(BeanDefinition.builder() + .name(beanName) + .typeFqn(context.getFqn(node)) + .qualifiers(qualifiers) + .isPrimary(isPrimary) + .declaringClass(node) + .build()); + } + + return super.visit(node); + } + + @Override + public boolean visit(MethodDeclaration node) { + if (!(node.getParent() instanceof TypeDeclaration parentTd)) { + return super.visit(node); + } + + boolean isBean = false; + boolean isPrimary = false; + String beanName = null; + List qualifiers = new ArrayList<>(); + + for (Object modifier : node.modifiers()) { + if (modifier instanceof Annotation annotation) { + String typeName = annotation.getTypeName().getFullyQualifiedName(); + if (typeName.endsWith("Bean")) { + isBean = true; + String value = extractAnnotationValue(annotation, "value"); + if (value.isEmpty()) { + value = extractAnnotationValue(annotation, "name"); + } + if (!value.isEmpty()) { + beanName = value.replace("\"", ""); + } + } else if (typeName.endsWith("Primary")) { + isPrimary = true; + } else if (typeName.endsWith("Qualifier")) { + String value = extractAnnotationValue(annotation, "value"); + if (!value.isEmpty()) { + qualifiers.add(value.replace("\"", "")); + } + } + } + } + + if (isBean && node.getReturnType2() != null) { + if (beanName == null) { + beanName = node.getName().getIdentifier(); + } + String returnType = extractTypeName(node.getReturnType2()); + // We need the FQN of the return type. Try resolving it using context. + String returnTypeFqn = resolveFqn(returnType, parentTd, context); + if (returnTypeFqn == null) { + returnTypeFqn = returnType; // fallback + } + + registry.register(BeanDefinition.builder() + .name(beanName) + .typeFqn(returnTypeFqn) + .qualifiers(qualifiers) + .isPrimary(isPrimary) + .declaringClass(parentTd) + .declaringMethod(node) + .build()); + } + + return super.visit(node); + } + }); + } + + private static String extractAnnotationValue(Annotation annotation, String memberName) { + if (annotation instanceof SingleMemberAnnotation sma) { + if ("value".equals(memberName) || "name".equals(memberName)) { // @Bean("name") uses SingleMemberAnnotation with value() + return sma.getValue().toString(); + } + } else if (annotation instanceof NormalAnnotation na) { + for (Object pairObj : na.values()) { + MemberValuePair pair = (MemberValuePair) pairObj; + if (pair.getName().getIdentifier().equals(memberName)) { + return pair.getValue().toString(); + } + } + } + return ""; + } + + private static String decapitalize(String name) { + if (name == null || name.isEmpty()) return name; + if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) { + return name; + } + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + private static String extractTypeName(Type type) { + if (type.isSimpleType()) { + return ((SimpleType) type).getName().getFullyQualifiedName(); + } else if (type.isParameterizedType()) { + return extractTypeName(((ParameterizedType) type).getType()); + } else if (type.isArrayType()) { + return extractTypeName(((ArrayType) type).getElementType()); + } + return type.toString(); + } + + private static String resolveFqn(String typeName, TypeDeclaration contextTd, CodebaseContext context) { + CompilationUnit cu = (CompilationUnit) contextTd.getRoot(); + TypeDeclaration td = context.getTypeDeclaration(typeName, cu); + if (td != null) { + return context.getFqn(td); + } + // Fallback to imports + for (Object impObj : cu.imports()) { + ImportDeclaration imp = (ImportDeclaration) impObj; + String impName = imp.getName().getFullyQualifiedName(); + if (impName.endsWith("." + typeName)) { + return impName; + } + } + return null; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java index 9f99830..8904b41 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java @@ -540,9 +540,23 @@ public class CallGraphBuilder { String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); - List impls = context.getImplementations(fallbackTypeFqn); - for (String impl : impls) { - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args)); + String exactImpl = null; + if (emr.getExpression() instanceof SimpleName sn) { + String varName = sn.getIdentifier(); + String qualifier = getQualifierForVariable(node, varName); + click.kamil.springstatemachineexporter.analysis.resolver.BeanDefinition bean = context.getBeanRegistry().resolve(fallbackTypeFqn, varName, qualifier); + if (bean != null) { + exactImpl = bean.getTypeFqn(); + } + } + + if (exactImpl != null) { + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(exactImpl + "." + emr.getName().getIdentifier(), args)); + } else { + List impls = context.getImplementations(fallbackTypeFqn); + for (String impl : impls) { + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args)); + } } } } @@ -655,9 +669,24 @@ public class CallGraphBuilder { String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); - List impls = context.getImplementations(className); - for (String impl : impls) { - allResolved.add(impl + "." + methodName); + Expression receiver = node.getExpression(); + String exactImpl = null; + if (receiver instanceof SimpleName sn) { + String varName = sn.getIdentifier(); + String qualifier = getQualifierForVariable(node, varName); + click.kamil.springstatemachineexporter.analysis.resolver.BeanDefinition bean = context.getBeanRegistry().resolve(className, varName, qualifier); + if (bean != null) { + exactImpl = bean.getTypeFqn(); + } + } + + if (exactImpl != null) { + allResolved.add(exactImpl + "." + methodName); + } else { + List impls = context.getImplementations(className); + for (String impl : impls) { + allResolved.add(impl + "." + methodName); + } } } @@ -788,6 +817,53 @@ public class CallGraphBuilder { return null; } + private String getQualifierForVariable(ASTNode contextNode, String varName) { + MethodDeclaration enclosingMethod = findEnclosingMethod(contextNode); + if (enclosingMethod != null) { + for (Object paramObj : enclosingMethod.parameters()) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj; + if (svd.getName().getIdentifier().equals(varName)) { + return extractQualifier(svd.modifiers()); + } + } + } + + TypeDeclaration enclosingType = findEnclosingType(contextNode); + if (enclosingType != null) { + for (FieldDeclaration field : enclosingType.getFields()) { + for (Object fragObj : field.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(varName)) { + return extractQualifier(field.modifiers()); + } + } + } + } + return null; + } + + private String extractQualifier(List modifiers) { + for (Object mod : modifiers) { + if (mod instanceof Annotation ann) { + String name = ann.getTypeName().getFullyQualifiedName(); + if (name.endsWith("Qualifier")) { + if (ann instanceof SingleMemberAnnotation sma) { + return sma.getValue().toString().replace("\"", ""); + } + if (ann instanceof NormalAnnotation na) { + for (Object pairObj : na.values()) { + MemberValuePair pair = (MemberValuePair) pairObj; + if (pair.getName().getIdentifier().equals("value")) { + return pair.getValue().toString().replace("\"", ""); + } + } + } + } + } + } + return null; + } + private List findPath(String start, String target, Map> graph, Set visited) { if (start.equals(target)) return new ArrayList<>(List.of(start)); if (!visited.add(start)) return null; // Path-scoped cycle detection @@ -818,9 +894,19 @@ public class CallGraphBuilder { } private boolean isHeuristicMatch(String neighbor, String target) { + if (neighbor.equals(target)) return true; if (target.endsWith("." + neighbor)) return true; - String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target; - String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; + if (neighbor.endsWith("." + target)) return true; + + boolean neighborHasClass = neighbor.contains("."); + boolean targetHasClass = target.contains("."); + + if (neighborHasClass && targetHasClass) { + return false; + } + + String simpleTarget = targetHasClass ? target.substring(target.lastIndexOf('.') + 1) : target; + String simpleNeighbor = neighborHasClass ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; return simpleNeighbor.equals(simpleTarget); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 4a8557d..44a998b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.*; +import click.kamil.springstatemachineexporter.analysis.resolver.BeanRegistry; +import click.kamil.springstatemachineexporter.analysis.resolver.BeanScanner; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +33,7 @@ public class CodebaseContext { private final Set ambiguousSimpleNames = new HashSet<>(); private final ConstantResolver constantResolver = new ConstantResolver(); private final PropertyResolver propertyResolver = new PropertyResolver(); + private final BeanRegistry beanRegistry = new BeanRegistry(this); private final List activeProfiles = new ArrayList<>(); private Map> allProperties = new HashMap<>(); private List libraryHints = new ArrayList<>(); @@ -167,6 +170,10 @@ public class CodebaseContext { scan(Set.of(rootDir), Collections.emptySet()); } + public BeanRegistry getBeanRegistry() { + return beanRegistry; + } + private final List allCus = new ArrayList<>(); private final Map> interfaceToImpls = new HashMap<>(); @@ -194,6 +201,11 @@ public class CodebaseContext { } } } + + // Post-processing: scan all parsed CompilationUnits for Spring beans + for (CompilationUnit cu : allCus) { + BeanScanner.scanAndRegister(cu, this, beanRegistry); + } } private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) { @@ -615,14 +627,31 @@ public class CodebaseContext { } } - // If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly - if (!isAbstract && extendsStateMachineConfigurerAdapter(td)) { + // If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly or has configurer methods + if (!isAbstract && (extendsStateMachineConfigurerAdapter(td) || hasConfigurerMethod(td))) { return true; } return false; } + private boolean hasConfigurerMethod(TypeDeclaration td) { + for (MethodDeclaration method : td.getMethods()) { + if (method.parameters().isEmpty()) continue; + for (Object paramObj : method.parameters()) { + if (paramObj instanceof SingleVariableDeclaration param) { + String paramType = param.getType().toString(); + if (paramType.contains("StateMachineTransitionConfigurer") || + paramType.contains("StateMachineStateConfigurer") || + paramType.contains("StateMachineConfigurationConfigurer")) { + return true; + } + } + } + } + return false; + } + public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) { return extendsStateMachineConfigurerAdapter(td, new HashSet<>()); } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java index f789b06..23740f4 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java @@ -27,13 +27,18 @@ public class GoldenUpdater { public static void main(String[] args) throws Exception { List scenarios = provideTestScenarios(); - Path rootDir = Path.of("state_machine_exporter"); + Path projectRoot = Path.of(System.getProperty("user.dir")); + if (projectRoot.endsWith("state_machine_exporter")) { + projectRoot = projectRoot.getParent(); + } + Path rootDir = projectRoot.resolve("state_machine_exporter"); for (TestScenario scenario : scenarios) { System.out.println("Updating golden files for: " + scenario.name()); Path tempDir = Files.createTempDirectory("golden-update-"); try { - exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true); + Path inputPath = projectRoot.resolve(scenario.inputPath()); + exportService.runExporter(inputPath, tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles()); List generatedDirs; try (var stream = Files.list(tempDir)) { @@ -158,6 +163,49 @@ public class GoldenUpdater { Path.of("state_machines/inheritance_extra_functions3_state_machine"), Path.of("src/test/resources/golden/G2StateMachineConfiguration"), "G2StateMachineConfiguration" + ), + new TestScenario( + "Extended Analysis Sample", + Path.of("state_machines/extended_analysis_sample"), + Path.of("src/test/resources/golden/ExtendedStateMachineConfig"), + "ExtendedStateMachineConfig" + ), + new TestScenario( + "Extended Analysis Sample (PROD)", + Path.of("state_machines/extended_analysis_sample"), + Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"), + "ExtendedStateMachineConfig", + List.of("prod") + ), + new TestScenario( + "Inheritance Sample", + Path.of("state_machines/inheritance_sample"), + Path.of("src/test/resources/golden/InheritanceStateMachineConfig"), + "InheritanceStateMachineConfig" + ), + new TestScenario( + "Enterprise Order System", + Path.of("state_machines/enterprise_order_system"), + Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"), + "EnterpriseStateMachineConfig" + ), + new TestScenario( + "Multi-Module Sample", + Path.of("state_machines/multi_module_sample/core-module"), + Path.of("src/test/resources/golden/OrderStateMachineConfig"), + "OrderStateMachineConfig" + ), + new TestScenario( + "Maven Multi-Module Sample", + Path.of("state_machines/maven_multi_module/core-module"), + Path.of("src/test/resources/golden/MavenOrderStateMachine"), + "MavenOrderStateMachine" + ), + new TestScenario( + "Complex Multi-Module Sample", + Path.of("state_machines/complex_multi_module_sm"), + Path.of("src/test/resources/golden/StateMachineConfig"), + "StateMachineConfig" ) ); } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistryTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistryTest.java new file mode 100644 index 0000000..a7da50c --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/BeanRegistryTest.java @@ -0,0 +1,123 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class BeanRegistryTest { + + private TestCodebaseContext context; + private BeanRegistry registry; + + static class TestCodebaseContext extends CodebaseContext { + private final Map> impls = new HashMap<>(); + + public void setImpls(String fqn, List implList) { + impls.put(fqn, implList); + } + + @Override + public List getImplementations(String fqn) { + return impls.getOrDefault(fqn, List.of()); + } + } + + @BeforeEach + void setUp() { + context = new TestCodebaseContext(); + registry = new BeanRegistry(context); + } + + @Test + void resolve_shouldMatchByExactType() { + registry.register(BeanDefinition.builder() + .name("paymentService") + .typeFqn("com.example.PaymentService") + .qualifiers(List.of()) + .isPrimary(false) + .build()); + + BeanDefinition resolved = registry.resolve("com.example.PaymentService", null, null); + assertThat(resolved).isNotNull(); + assertThat(resolved.getName()).isEqualTo("paymentService"); + } + + @Test + void resolve_shouldMatchByQualifierBypassingNameAndType() { + registry.register(BeanDefinition.builder() + .name("emailNotifier") + .typeFqn("com.example.EmailNotifier") + .qualifiers(List.of("email")) + .isPrimary(false) + .build()); + + registry.register(BeanDefinition.builder() + .name("smsNotifier") + .typeFqn("com.example.SmsNotifier") + .qualifiers(List.of("sms")) + .isPrimary(false) + .build()); + + context.setImpls("com.example.Notifier", List.of("com.example.EmailNotifier", "com.example.SmsNotifier")); + + BeanDefinition resolved = registry.resolve("com.example.Notifier", "emailNotifier", "sms"); + + assertThat(resolved).isNotNull(); + assertThat(resolved.getName()).isEqualTo("smsNotifier"); + } + + @Test + void resolve_shouldMatchByNameWhenNoQualifier() { + registry.register(BeanDefinition.builder() + .name("fastProcessor") + .typeFqn("com.example.FastProcessor") + .qualifiers(List.of()) + .isPrimary(false) + .build()); + + registry.register(BeanDefinition.builder() + .name("slowProcessor") + .typeFqn("com.example.SlowProcessor") + .qualifiers(List.of()) + .isPrimary(false) + .build()); + + context.setImpls("com.example.Processor", List.of("com.example.FastProcessor", "com.example.SlowProcessor")); + + BeanDefinition resolved = registry.resolve("com.example.Processor", "fastProcessor", null); + + assertThat(resolved).isNotNull(); + assertThat(resolved.getName()).isEqualTo("fastProcessor"); + } + + @Test + void resolve_shouldMatchByPrimary() { + registry.register(BeanDefinition.builder() + .name("localStorage") + .typeFqn("com.example.LocalStorage") + .qualifiers(List.of()) + .isPrimary(false) + .build()); + + registry.register(BeanDefinition.builder() + .name("cloudStorage") + .typeFqn("com.example.CloudStorage") + .qualifiers(List.of()) + .isPrimary(true) + .build()); + + context.setImpls("com.example.Storage", List.of("com.example.LocalStorage", "com.example.CloudStorage")); + + // Name doesn't match either, no qualifier + BeanDefinition resolved = registry.resolve("com.example.Storage", "storage", null); + + assertThat(resolved).isNotNull(); + assertThat(resolved.getName()).isEqualTo("cloudStorage"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java index 6e651af..76892e0 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java @@ -1081,7 +1081,6 @@ class CallGraphBuilderTest { assertThat(chains).hasSize(1); assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B"); } - @Test void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException { @@ -1897,4 +1896,73 @@ class CallGraphBuilderTest { org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST"); } + @Test + void shouldNarrowPolymorphicCallUsingBeanResolution(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + import org.springframework.stereotype.Component; + import org.springframework.beans.factory.annotation.Qualifier; + + public interface Notifier { + void send(String event); + } + + @Component("emailNotifier") + class EmailNotifier implements Notifier { + public void send(String event) { + // Email implementation + } + } + + @Component("smsNotifier") + class SmsNotifier implements Notifier { + public void send(String event) { + // SMS implementation + } + } + + @Component + public class OrderService { + private final Notifier smsNotifier; + + public OrderService(Notifier smsNotifier) { + this.smsNotifier = smsNotifier; + } + + public void processOrder() { + smsNotifier.send("ORDER_COMPLETED"); + } + } + """; + Files.writeString(tempDir.resolve("OrderService.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderService") + .methodName("processOrder") + .build(); + + TriggerPoint triggerEmail = TriggerPoint.builder() + .className("com.example.EmailNotifier") + .methodName("send") + .event("event") + .build(); + + TriggerPoint triggerSms = TriggerPoint.builder() + .className("com.example.SmsNotifier") + .methodName("send") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(triggerEmail, triggerSms)); + + // Because 'smsNotifier' perfectly matches the component name "smsNotifier", + // the bean resolution should narrow the call graph to ONLY SmsNotifier. + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getClassName()).isEqualTo("com.example.SmsNotifier"); + } } \ No newline at end of file diff --git a/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.png b/state_machine_exporter/src/test/resources/golden/MavenOrderStateMachine/MavenOrderStateMachine.png new file mode 100644 index 0000000000000000000000000000000000000000..0fc8768063049f15b4d7212dede8d2624bb51aa1 GIT binary patch literal 9017 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 MjFNPzq@n-+0opXgL;wH) literal 0 HcmV?d00001 diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.png new file mode 100644 index 0000000000000000000000000000000000000000..ba58e58ff6e1fe1b7623d278b3b5f40a8ce77c9b GIT binary patch literal 10017 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 TM(aSDFWWIoJKR02cijH~m_80P literal 0 HcmV?d00001