diff --git a/src/TODO.md b/src/TODO.md index 17d7b6c..5e71d28 100644 --- a/src/TODO.md +++ b/src/TODO.md @@ -1,4 +1,5 @@ 1. extract state machines from this as well +2. extract from all possible formats (ask chatgpt) ```java @@ -49,4 +50,112 @@ public class DynamicStateMachineExample { } } +``` + +1. define how java function can be implemented + 1.2. for this analyse bytecode manipulation such as project lombok +2. get a diagram +3. get a diagram with slicing, for example, slice by setValue on an ENUM field -> identify saga + + +1. make a list of all messages on message queues, how they're remapped and then who receives them + + + +NOTE +```java + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import java.io.File; +import java.io.FileInputStream; +import java.util.*; + +public class MethodCallGraphGenerator { + + static class MethodNode { + String className; + String methodName; + + public MethodNode(String className, String methodName) { + this.className = className; + this.methodName = methodName; + } + + @Override + public String toString() { + return className + "." + methodName; + } + + @Override + public int hashCode() { + return Objects.hash(className, methodName); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MethodNode other) { + return this.className.equals(other.className) && this.methodName.equals(other.methodName); + } + return false; + } + } + + static Map> callGraph = new HashMap<>(); + + public static void main(String[] args) throws Exception { + File sourceDir = new File("src"); // Change to your Java source directory + parseDirectory(sourceDir); + + generateDotFile("callgraph.dot"); + System.out.println("Call graph generated to callgraph.dot"); + } + + private static void parseDirectory(File dir) throws Exception { + for (File file : Objects.requireNonNull(dir.listFiles())) { + if (file.isDirectory()) { + parseDirectory(file); + } else if (file.getName().endsWith(".java")) { + parseFile(file); + } + } + } + + private static void parseFile(File file) throws Exception { + CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(file)); + cu.findAll(MethodDeclaration.class).forEach(method -> { + String className = cu.getPrimaryTypeName().orElse("Unknown"); + MethodNode caller = new MethodNode(className, method.getNameAsString()); + + callGraph.putIfAbsent(caller, new HashSet<>()); + + method.accept(new VoidVisitorAdapter() { + @Override + public void visit(MethodCallExpr call, Void arg) { + super.visit(call, arg); + String calleeName = call.getNameAsString(); + MethodNode callee = new MethodNode("Unknown", calleeName); // Could be refined with better context + callGraph.get(caller).add(callee); + } + }, null); + }); + } + + private static void generateDotFile(String filename) throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append("digraph G {\n"); + for (var caller : callGraph.keySet()) { + for (var callee : callGraph.get(caller)) { + sb.append(" \"").append(caller).append("\" -> \"").append(callee).append("\";\n"); + } + } + sb.append("}"); + java.nio.file.Files.writeString(new File(filename).toPath(), sb.toString()); + } +} + ``` \ No newline at end of file diff --git a/src/main/java/click/kamil/callgraphexporter/JdtResolvedCallGraph.java b/src/main/java/click/kamil/callgraphexporter/JdtResolvedCallGraph.java new file mode 100644 index 0000000..e307a97 --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/JdtResolvedCallGraph.java @@ -0,0 +1,159 @@ +package click.kamil.callgraphexporter; + +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; + +public class JdtResolvedCallGraph { + + // Use signature as the unique key + static Map> callGraph = new HashMap<>(); + + // Optionally store metadata + static Map metadataMap = new HashMap<>(); + + public static void main(String[] args) throws IOException { + File sourceDir = new File("src/main/java/click/kamil/callgraphexporter/simple"); + + List javaFiles = new ArrayList<>(); + collectJavaFiles(sourceDir, javaFiles); + + for (File file : javaFiles) { + parseFileWithBinding(file, sourceDir); + } + + generateDotFile("callgraph.dot"); + System.out.println("Call graph saved to callgraph.dot"); + } + + private static void collectJavaFiles(File dir, List javaFiles) { + for (File file : Objects.requireNonNull(dir.listFiles())) { + if (file.isDirectory()) { + collectJavaFiles(file, javaFiles); + } else if (file.getName().endsWith(".java")) { + javaFiles.add(file); + } + } + } + + private static void parseFileWithBinding(File file, File sourceRoot) throws IOException { + String source = Files.readString(file.toPath()); + + ASTParser parser = ASTParser.newParser(AST.JLS21); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + parser.setResolveBindings(true); + parser.setBindingsRecovery(true); + + parser.setEnvironment( + null, + new String[]{sourceRoot.getAbsolutePath()}, + null, + true + ); + + parser.setUnitName(file.getName()); + + Map options = JavaCore.getOptions(); + options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_17); + parser.setCompilerOptions(options); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + + cu.accept(new ASTVisitor() { + @Override + public boolean visit(MethodDeclaration method) { + IMethodBinding binding = method.resolveBinding(); + if (binding == null) return super.visit(method); + + MethodSignature callerSig = toSignature(binding); + MethodMetadata callerMeta = toMetadata(binding, method); + metadataMap.put(callerSig, callerMeta); + callGraph.putIfAbsent(callerSig, new HashSet<>()); + + method.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + IMethodBinding calleeBinding = node.resolveMethodBinding(); + if (calleeBinding != null) { + MethodSignature calleeSig = toSignature(calleeBinding); + callGraph.get(callerSig).add(calleeSig); + // You can collect metadata for callees too if needed + } + return super.visit(node); + } + }); + + return super.visit(method); + } + }); + } + + private static MethodSignature toSignature(IMethodBinding binding) { + String packageName = binding.getDeclaringClass().getPackage().getName(); + String className = binding.getDeclaringClass().getName(); + String methodName = binding.getName(); + + List paramTypes = new ArrayList<>(); + for (ITypeBinding param : binding.getParameterTypes()) { + paramTypes.add(param.getName()); + } + + return new MethodSignature(packageName, className, methodName, paramTypes); + } + + private static MethodMetadata toMetadata(IMethodBinding binding, MethodDeclaration method) { + String returnType = binding.getReturnType().getName(); + + Set exceptions = new HashSet<>(); + for (ITypeBinding ex : binding.getExceptionTypes()) { + exceptions.add(ex.getName()); + } + + Set modifiers = extractModifiers(binding, method); + List annotations = new ArrayList<>(); + + return new MethodMetadata(returnType, exceptions, modifiers, annotations, null); + } + + private static void generateDotFile(String filename) throws IOException { + StringBuilder sb = new StringBuilder("digraph G {\n"); + for (MethodSignature caller : callGraph.keySet()) { + for (MethodSignature callee : callGraph.get(caller)) { + sb.append(" \"").append(caller).append("\" -> \"").append(callee).append("\";\n"); + } + } + sb.append("}"); + Files.writeString(new File(filename).toPath(), sb.toString()); + } + private static Set extractModifiers(IMethodBinding binding, MethodDeclaration method) { + Set result = new HashSet<>(); + int mods = binding.getModifiers(); + + // 1. From bitmask + if (Modifier.isPublic(mods)) result.add(Modifiers.PUBLIC); + if (Modifier.isPrivate(mods)) result.add(Modifiers.PRIVATE); + if (Modifier.isProtected(mods)) result.add(Modifiers.PROTECTED); + if (Modifier.isStatic(mods)) result.add(Modifiers.STATIC); + if (Modifier.isAbstract(mods)) result.add(Modifiers.ABSTRACT); + if (Modifier.isFinal(mods)) result.add(Modifiers.FINAL); + if (Modifier.isNative(mods)) result.add(Modifiers.NATIVE); + if (Modifier.isSynchronized(mods)) result.add(Modifiers.SYNCHRONIZED); + if (Modifier.isStrictfp(mods)) result.add(Modifiers.STRICTFP); + + // 2. From modifiers list for special cases + for (Object mod : method.modifiers()) { + if (mod instanceof Modifier keyword) { + if (keyword.isDefault()) result.add(Modifiers.DEFAULT); + // NOTE: isSealed and isNonSealed are class-level only + } + // If needed, check for annotation modifiers here + } + + return result; + } +} diff --git a/src/main/java/click/kamil/callgraphexporter/Method.java b/src/main/java/click/kamil/callgraphexporter/Method.java new file mode 100644 index 0000000..415ef24 --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/Method.java @@ -0,0 +1,15 @@ +package click.kamil.callgraphexporter; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Method { + private MethodSignature signature; + private MethodMetadata metadata; +} diff --git a/src/main/java/click/kamil/callgraphexporter/MethodMetadata.java b/src/main/java/click/kamil/callgraphexporter/MethodMetadata.java new file mode 100644 index 0000000..237b6d9 --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/MethodMetadata.java @@ -0,0 +1,12 @@ +package click.kamil.callgraphexporter; + +import java.util.List; +import java.util.Set; + +public record MethodMetadata( + String returnType, + Set thrownExceptions, + Set modifiers, + List annotations, + String documentationComment // Optional: Javadoc +) {} \ No newline at end of file diff --git a/src/main/java/click/kamil/callgraphexporter/MethodSignature.java b/src/main/java/click/kamil/callgraphexporter/MethodSignature.java new file mode 100644 index 0000000..f0e7630 --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/MethodSignature.java @@ -0,0 +1,11 @@ +package click.kamil.callgraphexporter; + +import java.util.List; + +public record MethodSignature( + String packageName, + String className, + String methodName, + List parameterTypes +) { +} \ No newline at end of file diff --git a/src/main/java/click/kamil/callgraphexporter/Modifiers.java b/src/main/java/click/kamil/callgraphexporter/Modifiers.java new file mode 100644 index 0000000..36e9a1a --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/Modifiers.java @@ -0,0 +1,19 @@ +package click.kamil.callgraphexporter; + +public enum Modifiers { + PUBLIC, + PROTECTED, + PRIVATE, + STATIC, + ABSTRACT, + FINAL, + NATIVE, + SYNCHRONIZED, + TRANSIENT, + VOLATILE, + STRICTFP, + DEFAULT, + SEALED, + NON_SEALED, +} + diff --git a/src/main/java/click/kamil/callgraphexporter/simple/Kamil.java b/src/main/java/click/kamil/callgraphexporter/simple/Kamil.java new file mode 100644 index 0000000..32b3906 --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/simple/Kamil.java @@ -0,0 +1,12 @@ +package click.kamil.callgraphexporter.simple; + +public class Kamil { + + void a(){ + b(); + } + void b(){ + c(); + } + void c(){} +} diff --git a/src/main/java/click/kamil/callgraphexporter/simple/Kamil2.java b/src/main/java/click/kamil/callgraphexporter/simple/Kamil2.java new file mode 100644 index 0000000..279b330 --- /dev/null +++ b/src/main/java/click/kamil/callgraphexporter/simple/Kamil2.java @@ -0,0 +1,8 @@ +package click.kamil.callgraphexporter.simple; + +public class Kamil2 { + void ka() { + new Kamil().b(); + } + +} diff --git a/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java b/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java index c9fd857..dfb7a14 100644 --- a/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java +++ b/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java @@ -22,6 +22,7 @@ public class PlantUml implements StateMachineExporter { default -> "000000"; }; } + private final String LAMBDA = "λ"; @Override public String export(List transitions, @@ -127,7 +128,7 @@ public class PlantUml implements StateMachineExporter { return ""; } if (useLambdaGuards && t.isLambdaGuard()) { - return "lambda"; + return LAMBDA; } return guard .replaceAll("[\\n\\r]+", " ") @@ -147,7 +148,7 @@ public class PlantUml implements StateMachineExporter { int size = Math.min(actions.size(), isLambdaFlags.size()); return IntStream.range(0, size) - .mapToObj(i -> (useLambdaGuards && isLambdaFlags.get(i)) ? "lambda" : actions.get(i)) + .mapToObj(i -> (useLambdaGuards && isLambdaFlags.get(i)) ? LAMBDA : actions.get(i)) .collect(Collectors.joining(", ")); } diff --git a/src/main/java/click/kamil/springstatemachineexporter/ast/out/Scxml.java b/src/main/java/click/kamil/springstatemachineexporter/ast/out/Scxml.java index 3920878..6f6c17b 100644 --- a/src/main/java/click/kamil/springstatemachineexporter/ast/out/Scxml.java +++ b/src/main/java/click/kamil/springstatemachineexporter/ast/out/Scxml.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Set; public class Scxml implements StateMachineExporter { + private static final String LAMBDA = "λ"; @Override public String export(List transitions, @@ -74,7 +75,9 @@ public class Scxml implements StateMachineExporter { private static String getGuardText(Transition t, boolean useLambdaGuards) { if (t.getGuard() == null || t.getGuard().isBlank()) return ""; - if (useLambdaGuards && t.isLambdaGuard()) return "lambda"; + if (useLambdaGuards && t.isLambdaGuard()) { + return LAMBDA; + } return t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim(); }