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 1e8c860..dd998ff 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 @@ -364,6 +364,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { if (type1 == null || type2 == null) return false; type1 = eraseGenerics(type1); type2 = eraseGenerics(type2); + if (type1.length() == 1 || type2.length() == 1) 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/resolver/SiblingDependencyResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java index c55f30d..32074ae 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java @@ -18,14 +18,28 @@ public class SiblingDependencyResolver { private static final Pattern MAVEN_DEPENDENCY = Pattern.compile("\\s*(.*?)\\s*(.*?)\\s*(.*?)", Pattern.DOTALL); public Set resolveSiblings(Path projectDir) { - Set siblings = new HashSet<>(); - try { - siblings.addAll(resolveGradleSiblings(projectDir)); - siblings.addAll(resolveMavenSiblings(projectDir)); - } catch (Exception e) { - log.warn("Error resolving sibling dependencies for {}", projectDir, e); + Set allSiblings = new HashSet<>(); + Queue queue = new LinkedList<>(); + queue.add(projectDir); + + while (!queue.isEmpty()) { + Path current = queue.poll(); + Set currentSiblings = new HashSet<>(); + try { + currentSiblings.addAll(resolveGradleSiblings(current)); + currentSiblings.addAll(resolveMavenSiblings(current)); + } catch (Exception e) { + log.warn("Error resolving sibling dependencies for {}", current, e); + } + + for (Path sibling : currentSiblings) { + if (!allSiblings.contains(sibling) && !sibling.equals(projectDir)) { + allSiblings.add(sibling); + queue.add(sibling); + } + } } - return siblings; + return allSiblings; } private Set resolveGradleSiblings(Path projectDir) throws IOException { 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 937a7c0..924ede1 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 @@ -524,9 +524,7 @@ public class GenericEventDetector { } private String[] resolveStateMachineTypeArguments(MethodInvocation node) { - System.out.println("resolveStateMachineTypeArguments CALLED for node: " + node); Expression receiver = node.getExpression(); - System.out.println("initial receiver: " + receiver); // Trace back the receiver if it's a builder chain while (receiver instanceof MethodInvocation mi) { @@ -557,22 +555,63 @@ public class GenericEventDetector { // Fallback: search enclosing class/method for variable declaration Type declType = findReceiverTypeManually(receiver, node); - System.out.println("findReceiverTypeManually returned: " + declType); if (declType instanceof ParameterizedType pt) { List typeArgs = pt.typeArguments(); - System.out.println("typeArgs size: " + typeArgs.size()); 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); - System.out.println("Resolved types: state=" + stateType + " event=" + eventType); + + if (stateType != null && stateType.length() == 1) { + stateType = resolveGenericTypeVariable(stateType, node, cu); + } + if (eventType != null && eventType.length() == 1) { + eventType = resolveGenericTypeVariable(eventType, node, cu); + } + return new String[]{stateType, eventType}; } } - System.out.println("Returning null, null"); return new String[]{null, null}; } + private String resolveGenericTypeVariable(String typeVarName, ASTNode node, CompilationUnit cu) { + TypeDeclaration enclosingClass = findEnclosingType(node); + if (enclosingClass == null) return typeVarName; + String className = context.getFqn(enclosingClass); + if (className == null) return typeVarName; + + List impls = context.getImplementations(className); + if (impls == null || impls.isEmpty()) return typeVarName; + + for (String implFqn : impls) { + org.eclipse.jdt.core.dom.AbstractTypeDeclaration implNode = context.getAbstractTypeDeclaration(implFqn); + if (implNode instanceof TypeDeclaration td) { + Type superclassType = td.getSuperclassType(); + if (superclassType instanceof ParameterizedType pt) { + Type baseType = pt.getType(); + String baseName = resolveTypeToFqn(baseType, (CompilationUnit) implNode.getRoot()); + if (className.equals(baseName) || className.endsWith("." + baseName)) { + int typeIndex = -1; + List typeParams = enclosingClass.typeParameters(); + for (int i = 0; i < typeParams.size(); i++) { + TypeParameter tp = (TypeParameter) typeParams.get(i); + if (tp.getName().getIdentifier().equals(typeVarName)) { + typeIndex = i; + break; + } + } + if (typeIndex >= 0 && typeIndex < pt.typeArguments().size()) { + Type concreteType = (Type) pt.typeArguments().get(typeIndex); + return resolveTypeToFqn(concreteType, (CompilationUnit) implNode.getRoot()); + } + } + } + } + } + return typeVarName; + } + private ITypeBinding findStateMachineSupertype(ITypeBinding binding, java.util.Set visited) { ITypeBinding current = binding; while (current != null) { diff --git a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/command/HtmlExporterCommand.java b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/command/HtmlExporterCommand.java index c2fb375..47d4b84 100644 --- a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/command/HtmlExporterCommand.java +++ b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/command/HtmlExporterCommand.java @@ -62,6 +62,9 @@ public class HtmlExporterCommand implements Callable { @Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false") private boolean debug; + @Option(names = {"--resolve-classpath"}, description = "Resolve external dependencies to enable deep JDT binding across modules.", defaultValue = "true", fallbackValue = "true") + private boolean resolveClasspath = true; + @Override public Integer call() throws Exception { var out = spec.commandLine().getOut(); @@ -97,7 +100,7 @@ public class HtmlExporterCommand implements Callable { exportService.runJsonExporter(jsonFile, outputDir, formats, profiles); } else { boolean renderChoicesAsDiamonds = !noDiamonds; - exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter); + exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, resolveClasspath); } System.clearProperty("html.hideMetadataPane");