From 9a5f122bd299eaf5c3cc69c8f9eec49cef3e5ca9 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 7 Jul 2026 04:50:18 +0200 Subject: [PATCH] ai idea --- .../service/AbstractCallGraphEngine.java | 57 ++++++++++++++++++- .../service/DynamicClasspathResolver.java | 38 ++++++++----- .../analysis/service/JdtCallGraphEngine.java | 6 +- .../analysis/service/VariableTracer.java | 40 ++++++++++++- .../service/DynamicClasspathResolverTest.java | 2 +- .../HeuristicCallGraphEngineCoreTest.java | 43 ++++++++++++++ 6 files changed, 165 insertions(+), 21 deletions(-) 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 adbca37..3558df0 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 @@ -27,10 +27,29 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (expr == null) return null; return parsedNodeCache.computeIfAbsent(expr, e -> { ASTParser parser = ASTParser.newParser(AST.JLS17); + Map options = org.eclipse.jdt.core.JavaCore.getOptions(); + org.eclipse.jdt.core.JavaCore.setComplianceOptions(org.eclipse.jdt.core.JavaCore.VERSION_17, options); + parser.setCompilerOptions(options); parser.setKind(ASTParser.K_EXPRESSION); parser.setSource(e.toCharArray()); try { - return parser.createAST(null); + ASTNode node = parser.createAST(null); + if (node instanceof org.eclipse.jdt.core.dom.CompilationUnit) { + ASTParser fallbackParser = ASTParser.newParser(AST.JLS17); + fallbackParser.setCompilerOptions(options); + fallbackParser.setKind(ASTParser.K_COMPILATION_UNIT); + fallbackParser.setSource(("class A { Object o = " + e + "; }").toCharArray()); + org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) fallbackParser.createAST(null); + if (!cu.types().isEmpty()) { + org.eclipse.jdt.core.dom.TypeDeclaration td = (org.eclipse.jdt.core.dom.TypeDeclaration) cu.types().get(0); + if (!td.bodyDeclarations().isEmpty() && td.bodyDeclarations().get(0) instanceof org.eclipse.jdt.core.dom.FieldDeclaration fd) { + if (!fd.fragments().isEmpty() && fd.fragments().get(0) instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf) { + return vdf.getInitializer(); + } + } + } + } + return node; } catch (Exception ex) { return null; } @@ -364,6 +383,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } ASTNode exprNode = parseExpressionString(resolvedValue); + while (exprNode instanceof ParenthesizedExpression pe) { + exprNode = pe.getExpression(); + } String varName = null; String methodName = null; @@ -495,6 +517,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } sourceMethod = "inline-ternary"; declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0); + } else if (exprNode instanceof SwitchExpression swExpr) { + List polyEventsRef = polymorphicEvents; + swExpr.accept(new ASTVisitor() { + @Override + public boolean visit(YieldStatement ys) { + List tracedBranch = variableTracer.traceVariableAll(ys.getExpression()); + for (Expression tb : tracedBranch) { + if (tb instanceof ClassInstanceCreation cic) { + polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType())); + } else { + constantExtractor.extractConstantsFromExpression(tb, polyEventsRef); + } + } + return super.visit(ys); + } + @Override + public boolean visit(ExpressionStatement es) { + List tracedBranch = variableTracer.traceVariableAll(es.getExpression()); + for (Expression tb : tracedBranch) { + if (tb instanceof ClassInstanceCreation cic) { + polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType())); + } else { + constantExtractor.extractConstantsFromExpression(tb, polyEventsRef); + } + } + return super.visit(es); + } + }); + sourceMethod = "inline-switch"; + declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0); } else if (exprNode instanceof SimpleName sn) { varName = sn.getIdentifier(); if (varName.equals(varName.toUpperCase())) { @@ -980,6 +1032,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { protected List evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List path, Map> callGraph) { ASTNode exprNode = parseExpressionString(resolvedValue); + while (exprNode instanceof ParenthesizedExpression pe) { + exprNode = pe.getExpression(); + } if (exprNode instanceof SuperMethodInvocation smi) { String getterName = smi.getName().getIdentifier(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolver.java index 16d3a44..a4ae675 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolver.java @@ -34,9 +34,7 @@ public class DynamicClasspathResolver { protected List resolveMaven(Path projectRoot) { log.info("Maven project detected. Resolving dependencies..."); - Path cpFile = null; try { - cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt"); String mvnCmd = getCommand(projectRoot, "mvnw", "mvn"); if (mvnCmd == null) { log.warn("Maven executable not found."); @@ -48,26 +46,32 @@ public class DynamicClasspathResolver { "-q", "dependency:build-classpath", "-DincludeScope=compile", - "-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString() + "-Dmdep.outputFile=sme_cp.txt" ); pb.directory(projectRoot.toFile()); runProcess(pb); - if (Files.exists(cpFile)) { - String cpString = Files.readString(cpFile).trim(); - if (!cpString.isEmpty()) { - return Arrays.asList(cpString.split(File.pathSeparator)); - } + java.util.Set allPaths = new java.util.LinkedHashSet<>(); + try (java.util.stream.Stream stream = Files.walk(projectRoot)) { + stream.filter(p -> p.getFileName().toString().equals("sme_cp.txt")) + .forEach(cpFile -> { + try { + String cpString = Files.readString(cpFile).trim(); + if (!cpString.isEmpty()) { + allPaths.addAll(Arrays.asList(cpString.split(File.pathSeparator))); + } + Files.deleteIfExists(cpFile); + } catch (IOException e) { + log.warn("Failed to read or delete sme_cp.txt at {}", cpFile, e); + } + }); + } + if (!allPaths.isEmpty()) { + return new ArrayList<>(allPaths); } } catch (Exception e) { log.error("Failed to dynamically resolve Maven classpath", e); - } finally { - if (cpFile != null) { - try { - Files.deleteIfExists(cpFile); - } catch (IOException ignored) {} - } } return Collections.emptyList(); } @@ -107,14 +111,18 @@ public class DynamicClasspathResolver { pb.directory(projectRoot.toFile()); String output = runProcessAndGetOutput(pb); + java.util.Set allPaths = new java.util.LinkedHashSet<>(); for (String line : output.split("\\r?\\n")) { if (line.startsWith("SME_CLASSPATH_MARKER:")) { String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim(); if (!cpString.isEmpty()) { - return Arrays.asList(cpString.split(File.pathSeparator)); + allPaths.addAll(Arrays.asList(cpString.split(File.pathSeparator))); } } } + if (!allPaths.isEmpty()) { + return new ArrayList<>(allPaths); + } } catch (Exception e) { log.error("Failed to dynamically resolve Gradle classpath", e); } finally { 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 9520ce5..ad8eb26 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 @@ -162,7 +162,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { } // Delegate to base class for lambda, method reference, and constructor unwrapping - return super.resolveArgument(expr); + String result = super.resolveArgument(expr); + if (result != null) { + result = result.replaceAll("->\\s*yield\\s+", "-> "); + } + return result; } protected String resolveCalledMethod(MethodInvocation node) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java index 3869825..477522f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java @@ -110,11 +110,43 @@ public class VariableTracer { if (superFqn != null) { TypeDeclaration superTd = context.getTypeDeclaration(superFqn); if (superTd != null) { - return getFieldType(superTd, fieldName, context, visited); + String result = getFieldType(superTd, fieldName, context, visited); + if (result != null) return result; } } + + for (Object intfObj : td.superInterfaceTypes()) { + Type intfType = (Type) intfObj; + String intfName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(intfType); + String resolvedIntf = resolveTypeFqn(intfName, context, td); + if (resolvedIntf != null) { + TypeDeclaration intfTd = context.getTypeDeclaration(resolvedIntf); + if (intfTd != null) { + String result = getFieldType(intfTd, fieldName, context, visited); + if (result != null) return result; + } + } + } + return null; } + + private String resolveTypeFqn(String simpleName, CodebaseContext context, TypeDeclaration td) { + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + if (cu != null) { + for (Object impObj : cu.imports()) { + ImportDeclaration imp = (ImportDeclaration) impObj; + String name = imp.getName().getFullyQualifiedName(); + if (name.endsWith("." + simpleName)) { + return name; + } + } + if (cu.getPackage() != null) { + return cu.getPackage().getName().getFullyQualifiedName() + "." + simpleName; + } + } + return simpleName; + } public String getVariableDeclaredType(String methodFqn, String cleanFieldName) { if (methodFqn == null) return null; @@ -294,14 +326,16 @@ public class VariableTracer { stringified.add(traced.toString()); } } - if (stringified.size() == 1) return stringified.get(0); + if (stringified.size() == 1) { + return stringified.get(0).replaceAll("->\\s*yield\\s+", "-> "); + } StringBuilder sb = new StringBuilder(); for (int i = 0; i < stringified.size() - 1; i++) { sb.append("true ? ").append(stringified.get(i)).append(" : "); } sb.append(stringified.get(stringified.size() - 1)); - return sb.toString(); + return sb.toString().replaceAll("->\\s*yield\\s+", "-> "); } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolverTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolverTest.java index a637eed..2f77a2b 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolverTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DynamicClasspathResolverTest.java @@ -31,7 +31,7 @@ class DynamicClasspathResolverTest { .filter(arg -> arg.startsWith("-Dmdep.outputFile=")) .findFirst() .orElseThrow(); - Path outputFile = Path.of(outputFileArg.substring("-Dmdep.outputFile=".length())); + Path outputFile = pb.directory().toPath().resolve(outputFileArg.substring("-Dmdep.outputFile=".length())); Files.writeString(outputFile, cp); } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java index c174242..49ccdab 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java @@ -1036,4 +1036,47 @@ class HeuristicCallGraphEngineCoreTest { org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("REACTIVE_EVENT"); } + + @Test + void shouldExtractEventFromSwitchExpressionAndParentheses(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class SwitchOrderService { + public void processOrder(int type) { + String e = (((switch(type) { + case 1 -> "PAY"; + default -> { yield "CANCEL"; } + }))); + sendEvent(e); + } + + public void sendEvent(String event) { + } + } + """; + Files.writeString(tempDir.resolve("SwitchOrderService.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.SwitchOrderService") + .methodName("processOrder") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.SwitchOrderService") + .methodName("sendEvent") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + System.out.println("POLY EVENTS: " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("PAY", "CANCEL"); + } }