diff --git a/config.json b/config.json index 7d0aaf9..259007e 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,10 @@ { - "projectDirectory": "../LoopAntiPattern/src", - "repositoryDirectory": "../LoopAntiPattern/src/main/java/com/example/LoopAntiPattern/data/repository", - "excludedClasses": ["ProductRepository.java", "ExcludeThat.java"], + "projectDirectory": "../fromItestra/LoopAntiPattern/LoopAntiPattern/src", + "repositoryDirectory": "../fromItestra/LoopAntiPattern/LoopAntiPattern/src/main/java/com/example/LoopAntiPattern", + "excludedClasses": ["ExcludeThis.java", "ExcludeThat.java"], "excludedMethods": ["someMethod"] -} \ No newline at end of file +} + +//,"ProductRepository.java","ProductService.java" +//"repositoryDirectory": "../fromItestra/LoopAntiPattern/LoopAntiPattern/src/main/java/com/example/LoopAntiPattern/data/repository", +//"repositoryDirectory": "../fromItestra/LoopAntiPattern/LoopAntiPattern/src/main/java/com/example/LoopAntiPattern/service", diff --git a/pom.xml b/pom.xml index c9c62b7..6cb8cee 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,26 @@ jgrapht-core 1.5.1 + + org.jgrapht + jgrapht-io + 1.4.0 + + + io.github.classgraph + classgraph + 4.8.117 + + + org.springframework + spring-context + 5.3.1 + + + org.springframework + spring-tx + 5.3.1 + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/tum/dpid/AntiPatternDetector.java b/src/main/java/tum/dpid/AntiPatternDetector.java new file mode 100644 index 0000000..1e37bac --- /dev/null +++ b/src/main/java/tum/dpid/AntiPatternDetector.java @@ -0,0 +1,176 @@ +package tum.dpid; + +import org.eclipse.jdt.core.dom.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; + +/* +* CompilationUnit +└── TypeDeclaration (class Example) + ├── MethodDeclaration (methodA) + │ └── Block + │ └── ForStatement + │ ├── Expression (int i = 0) + │ ├── Expression (i < 10) + │ ├── Expression (i++) + │ └── Block + │ └── MethodInvocation (methodB) + ├── MethodDeclaration (methodB) + │ └── Block + │ └── MethodInvocation (methodC) + └── MethodDeclaration (methodC) + └── Block + └── MethodInvocation (find) +*/ + +public class AntiPatternDetector { + + private static final List DB_METHODS = List.of("flush", "save", "update", "persist", "remove", "find"); + + public static void main(String[] args) { + String projectDirectoryPath = "../fromItestra/LoopAntiPattern"; + + File projectDirectory = new File(projectDirectoryPath); + if (!projectDirectory.exists() || !projectDirectory.isDirectory()) { + System.out.println("Invalid project directory path."); + return; + } + + Map methodMap = collectMethods(projectDirectory); + + for (MethodDeclaration method : methodMap.values()) { + method.accept(new ForLoopVisitor(methodMap)); + } + } + + private static Map collectMethods(File projectDirectory) { + Map methodMap = new HashMap<>(); + List javaFiles = getJavaFiles(projectDirectory); + + if (javaFiles != null) { + for (File javaFile : javaFiles) { + try { + String source = new String(Files.readAllBytes(javaFile.toPath())); + ASTParser parser = ASTParser.newParser(AST.JLS_Latest); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + cu.accept(new MethodCollector(methodMap)); + } catch (IOException e) { + System.err.println("Error reading file: " + javaFile.getName()); + e.printStackTrace(); + } + } + } + + return methodMap; + } + + private static List getJavaFiles(File directory) { + List javaFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + javaFiles.addAll(getJavaFiles(file)); + } else if (file.isFile() && file.getName().endsWith(".java")) { + javaFiles.add(file); + } + } + } + return javaFiles; + } + + private static class MethodCollector extends ASTVisitor { + private final Map methodMap; + + public MethodCollector(Map methodMap) { + this.methodMap = methodMap; + } + + @Override + public boolean visit(TypeDeclaration node) { + for (MethodDeclaration method : node.getMethods()) { + String methodName = method.getName().getIdentifier(); + methodMap.put(methodName, method); + } + return super.visit(node); + } + } + + private static class ForLoopVisitor extends ASTVisitor { + private final Map methodMap; + + public ForLoopVisitor(Map methodMap) { + this.methodMap = methodMap; + } + + @Override + public boolean visit(MethodDeclaration node) { + node.accept(new ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.ForStatement forStatement) { + forStatement.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation methodInvocation) { + String methodName = methodInvocation.getName().getIdentifier(); + if (DB_METHODS.contains(methodName)) { + reportAntiPattern(methodInvocation); + } else if (methodMap.containsKey(methodName)) { + MethodDeclaration methodDeclaration = methodMap.get(methodName); + Set visitedMethods = new HashSet<>(); + visitedMethods.add(node.getName().getIdentifier()); + methodDeclaration.accept(new MethodInvocationVisitor(methodMap, methodInvocation, visitedMethods)); + } + return super.visit(methodInvocation); + } + }); + return super.visit(forStatement); + } + }); + return super.visit(node); + } + + private void reportAntiPattern(MethodInvocation methodInvocation) { + System.out.println("Anti-pattern detected: " + methodInvocation.getName().getIdentifier() + + " call inside a for loop at line " + + ((CompilationUnit) methodInvocation.getRoot()).getLineNumber(methodInvocation.getStartPosition())); + } + } + + private static class MethodInvocationVisitor extends ASTVisitor { + private final Map methodMap; + private final MethodInvocation originalInvocation; + private final Set visitedMethods; + + public MethodInvocationVisitor(Map methodMap, MethodInvocation originalInvocation, Set visitedMethods) { + this.methodMap = methodMap; + this.originalInvocation = originalInvocation; + this.visitedMethods = visitedMethods; + } + + @Override + public boolean visit(MethodInvocation methodInvocation) { + String methodName = methodInvocation.getName().getIdentifier(); + if (visitedMethods.contains(methodName)) { + return false; // Skip already visited methods in this call chain + } + visitedMethods.add(methodName); + + if (DB_METHODS.contains(methodName)) { + System.out.println("Anti-pattern detected: " + originalInvocation.getName().getIdentifier() + + " indirectly calls " + methodInvocation.getName().getIdentifier() + + " inside a for loop at line " + + ((CompilationUnit) originalInvocation.getRoot()).getLineNumber(originalInvocation.getStartPosition())); + } else if (methodMap.containsKey(methodName)) { + MethodDeclaration methodDeclaration = methodMap.get(methodName); + methodDeclaration.accept(new MethodInvocationVisitor(methodMap, originalInvocation, visitedMethods)); + } + return super.visit(methodInvocation); + } + } +} \ No newline at end of file diff --git a/src/main/java/tum/dpid/CallChainAnalyzer.java b/src/main/java/tum/dpid/CallChainAnalyzer.java index 124ed28..20ca3bc 100644 --- a/src/main/java/tum/dpid/CallChainAnalyzer.java +++ b/src/main/java/tum/dpid/CallChainAnalyzer.java @@ -13,24 +13,26 @@ public class CallChainAnalyzer { + private static final List DB_METHODS = List.of("flush", "save", "update", "persist", "remove", "find", + "selectById", "selectExistingById", "delete", + "selectByArticleNumber", "saveWithoutFlush", "merge", "selectFrom", "callSave"); + public static void main(String[] args) throws IOException { ObjectMapper mapper = new ObjectMapper(); AnalyzerConfig config; try { - //config file path will be read as a cmd line argument after building this tool as a jar application config = mapper.readValue(new File("config.json"), AnalyzerConfig.class); } catch (IOException e) { System.out.println("Error reading config file:\n" + e.getMessage()); System.exit(1); - return; //This is added to prevent "variable might not be initialized" warnings. This line is unreachable + return; } - // find and collect methods under repository folder to create a target list String directoryPath = config.getRepositoryDirectory(); Path dirPath = Paths.get(directoryPath); - List DB_METHODS = new ArrayList<>(); + List dbMethods = new ArrayList<>(DB_METHODS); try { List javaFiles = Files.walk(dirPath) @@ -39,7 +41,7 @@ public static void main(String[] args) throws IOException { .toList(); for (Path javaFile : javaFiles) { - DB_METHODS.addAll(extractMethodNames(javaFile, config.getExcludedClasses(), config.getExcludedMethods())); + dbMethods.addAll(extractMethodNames(javaFile, config.getExcludedClasses(), config.getExcludedMethods())); } } catch (IOException e) { e.printStackTrace(); @@ -49,31 +51,24 @@ public static void main(String[] args) throws IOException { File projectDirectory = new File(projectDirectoryPath); if (!projectDirectory.exists() || !projectDirectory.isDirectory()) { - System.out.println("Invalid project directory path."); + System.err.println("[ERROR] Invalid project directory path."); return; } - Map methodMap = collectMethods(projectDirectory, config.getExcludedMethods()); - Map> callGraph = buildCallGraph(methodMap); + Map methodMap = collectMethods(projectDirectory, config.getExcludedClasses(), config.getExcludedMethods()); + Map> callGraph = buildCallGraph(methodMap); - for (String targetMethod : DB_METHODS) { - List callChain = new ArrayList<>(); - traceCallChainInOrder(targetMethod, callGraph, new HashSet<>(), callChain, methodMap); - - if (callChain.isEmpty()) { - System.out.println("No calls found for method: " + targetMethod); - } - - System.out.println("Call graph for method: " + targetMethod); + for (String targetMethod : dbMethods) { + System.out.println("\nCall graph for method: " + targetMethod); drawCallGraph(targetMethod, callGraph, 0, new HashSet<>()); - System.out.println(); - } + + checkForDatabaseCallsInLoops(methodMap, callGraph); } private static List extractMethodNames(Path javaFilePath, List excludedClasses, List excludedMethods) throws IOException { String className = javaFilePath.getFileName().toString(); - if(excludedClasses.contains(className)) { + if (excludedClasses.contains(className)) { return Collections.emptyList(); } String content = new String(Files.readAllBytes(javaFilePath)); @@ -84,45 +79,31 @@ private static List extractMethodNames(Path javaFilePath, List e final CompilationUnit cu = (CompilationUnit) parser.createAST(null); MethodNameVisitor visitor = new MethodNameVisitor(excludedMethods); cu.accept(visitor); + System.out.println("New db methods found" + visitor.getMethodNames()); return visitor.getMethodNames(); } - static class MethodNameVisitor extends ASTVisitor { - private final List methodNames = new ArrayList<>(); - private final List excludedMethods; - - public MethodNameVisitor(List excludedMethods) { - this.excludedMethods = excludedMethods; - } - @Override - public boolean visit(MethodDeclaration node) { - if(!excludedMethods.contains(node.getName().getIdentifier())){ - methodNames.add(node.getName().getIdentifier()); - } - return super.visit(node); - } - - public List getMethodNames() { - return methodNames; - } - } - - private static Map collectMethods(File projectDirectory, List excludedMethods) throws IOException { + private static Map collectMethods(File projectDirectory, List excludedClasses, List excludedMethods) { Map methodMap = new HashMap<>(); List javaFiles = getJavaFiles(projectDirectory); - for (File javaFile : javaFiles) { - try { - String source = new String(Files.readAllBytes(javaFile.toPath())); - ASTParser parser = ASTParser.newParser(AST.JLS_Latest); - parser.setSource(source.toCharArray()); - parser.setKind(ASTParser.K_COMPILATION_UNIT); - - CompilationUnit cu = (CompilationUnit) parser.createAST(null); - cu.accept(new MethodCollector(methodMap, excludedMethods)); - } catch (IOException e) { - System.err.println("Error reading file: " + javaFile.getName()); - e.printStackTrace(); + if (javaFiles != null) { + for (File javaFile : javaFiles) { + String className = javaFile.getName(); + if (!excludedClasses.contains(className)) { + try { + String source = new String(Files.readAllBytes(javaFile.toPath())); + ASTParser parser = ASTParser.newParser(AST.JLS_Latest); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + cu.accept(new MethodCollector(methodMap, excludedMethods)); + } catch (IOException e) { + System.err.println("[ERROR] Error reading file: " + javaFile.getName()); + e.printStackTrace(); + } + } } } @@ -146,76 +127,143 @@ private static List getJavaFiles(File directory) { return javaFiles; } - private static Map> buildCallGraph(Map methodMap) { - Map> callGraph = new HashMap<>(); + private static Map> buildCallGraph(Map methodMap) { + Map> callGraph = new HashMap<>(); for (MethodDeclaration method : methodMap.values()) { - if(method.getName().toString().equals("updateProducts")) - { - System.out.println(""); - } method.accept(new ASTVisitor() { @Override public boolean visit(MethodInvocation node) { String caller = method.getName().toString(); String callee = node.getName().toString(); - callGraph.computeIfAbsent(callee, k -> new ArrayList<>()).add(caller); + callGraph.computeIfAbsent(callee, k -> new HashSet<>()).add(caller); + return super.visit(node); + } + }); + } + + return callGraph; + } + + private static void drawCallGraph(String methodName, Map> callGraph, int level, Set visited) { + if (!visited.add(methodName)) { + return; + } + printIndented(methodName, level); + Set callers = callGraph.get(methodName); + if (callers != null) { + for (String caller : callers) { + drawCallGraph(caller, callGraph, level + 1, visited); + } + } + } + + private static void printIndented(String methodName, int level) { + for (int i = 0; i < level; i++) { + System.out.print(" "); + } + System.out.println(methodName); + } + + private static void checkForDatabaseCallsInLoops(Map methodMap, Map> callGraph) { + for (MethodDeclaration method : methodMap.values()) { + method.accept(new ASTVisitor() { + @Override + public boolean visit(ForStatement node) { + checkMethodInvocationsInLoop(node.getBody(), methodMap, callGraph); + return super.visit(node); + } + + @Override + public boolean visit(WhileStatement node) { + checkMethodInvocationsInLoop(node.getBody(), methodMap, callGraph); + return super.visit(node); + } + + @Override + public boolean visit(DoStatement node) { + checkMethodInvocationsInLoop(node.getBody(), methodMap, callGraph); + return super.visit(node); + } + @Override + public boolean visit(EnhancedForStatement node) { + checkMethodInvocationsInLoop(node.getBody(), methodMap, callGraph); return super.visit(node); } @Override public boolean visit(LambdaExpression node) { - node.getBody().accept(new ASTVisitor() { - @Override - public boolean visit(MethodInvocation lambdaNode) { - String caller = method.getName().toString(); - String callee = lambdaNode.getName().toString(); - callGraph.computeIfAbsent(callee, k -> new ArrayList<>()).add(caller); - return super.visit(lambdaNode); - } - }); + checkMethodInvocationsInLoop(node.getBody(), methodMap, callGraph); + return super.visit(node); + } + @Override + public boolean visit(MethodInvocation node) { + // Check for stream method calls like map, filter, etc. + if (node.getName().getIdentifier().equals("map") || node.getName().getIdentifier().equals("forEach") || node.getName().getIdentifier().equals("stream")) { + node.accept(new ASTVisitor() { + @Override + public boolean visit(LambdaExpression lambdaExpression) { + checkMethodInvocationsInLoop(lambdaExpression.getBody(), methodMap, callGraph); + return super.visit(lambdaExpression); + } + + @Override + public boolean visit(MethodInvocation innerNode) { + String methodName = innerNode.getName().getIdentifier(); + if (DB_METHODS.contains(methodName)) { + reportAntiPattern(innerNode); + } else if (methodMap.containsKey(methodName)) { + traceMethodCallsInLoop(methodName, methodMap, callGraph, new HashSet<>(), innerNode); + } + return super.visit(innerNode); + } + }); + } return super.visit(node); } }); } + } - return callGraph; + private static void checkMethodInvocationsInLoop(ASTNode loopBody, Map methodMap, Map> callGraph) { + loopBody.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + String methodName = node.getName().getIdentifier(); + if (DB_METHODS.contains(methodName)) { + reportAntiPattern(node); + } else if (methodMap.containsKey(methodName)) { + traceMethodCallsInLoop(methodName, methodMap, callGraph, new HashSet<>(), node); + } + return super.visit(node); + } + }); } - private static void traceCallChainInOrder(String methodName, Map> callGraph, Set visited, List callChain, Map methodMap) { + private static void traceMethodCallsInLoop(String methodName, Map methodMap, Map> callGraph, Set visited, MethodInvocation originalInvocation) { if (!visited.add(methodName)) { return; } - callChain.add(methodName); - List callees = callGraph.get(methodName); + Set callees = callGraph.get(methodName); if (callees != null) { for (String callee : callees) { - traceCallChainInOrder(callee, callGraph, visited, callChain, methodMap); + if (DB_METHODS.contains(callee)) { + reportAntiPattern(originalInvocation); + } else if (methodMap.containsKey(callee)) { + traceMethodCallsInLoop(callee, methodMap, callGraph, visited, originalInvocation); + } } } } - private static void drawCallGraph(String methodName, Map> callGraph, int level, Set visited) { - if (!visited.add(methodName)) { - return; - } - printIndented(methodName, level); - List callers = callGraph.get(methodName); - if (callers != null) { - for (String caller : callers) { - drawCallGraph(caller, callGraph, level + 1, visited); - } - } - } + private static void reportAntiPattern(MethodInvocation methodInvocation) { + System.err.println("[ANTI-PATTERN] Detected: " + methodInvocation.getName().getIdentifier() + + " call inside a loop at line " + + ((CompilationUnit) methodInvocation.getRoot()).getLineNumber(methodInvocation.getStartPosition())); - private static void printIndented(String methodName, int level) { - for (int i = 0; i < level; i++) { - System.out.print("---- "); - } - System.out.println(methodName); } static class MethodCollector extends ASTVisitor { @@ -229,10 +277,33 @@ static class MethodCollector extends ASTVisitor { @Override public boolean visit(MethodDeclaration node) { - if(!excludedMethods.contains(node.getName().toString())) { + + if (!excludedMethods.contains(node.getName().toString())) { methodMap.put(node.getName().toString(), node); } return super.visit(node); } } + + static class MethodNameVisitor extends ASTVisitor { + private final List methodNames = new ArrayList<>(); + private final List excludedMethods; + + public MethodNameVisitor(List excludedMethods) { + this.excludedMethods = excludedMethods; + } + + @Override + public boolean visit(MethodDeclaration node) { + if (!excludedMethods.contains(node.getName().getIdentifier())) { + methodNames.add(node.getName().getIdentifier()); + } + return super.visit(node); + } + + public List getMethodNames() { + return methodNames; + } + } + } diff --git a/src/main/java/tum/dpid/DynamicCallChainAnalyzer.java b/src/main/java/tum/dpid/DynamicCallChainAnalyzer.java new file mode 100644 index 0000000..d2988f3 --- /dev/null +++ b/src/main/java/tum/dpid/DynamicCallChainAnalyzer.java @@ -0,0 +1,212 @@ +package com.itestra.callchain; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.util.*; + +import org.eclipse.jdt.core.dom.*; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; +import tum.dpid.CallChainAnalyzer; + +public class DynamicCallChainAnalyzer { + + private static List dbMethods; + + public static void main(String[] args) { + DynamicCallChainAnalyzer analyzer = new DynamicCallChainAnalyzer(); + analyzer.initializeDbMethods(); + String projectDirectoryPath = "../fromItestra/LoopAntiPattern"; + + File projectDirectory = new File(projectDirectoryPath); + if (!projectDirectory.exists() || !projectDirectory.isDirectory()) { + System.out.println("Invalid project directory path."); + return; + } + + Map methodMap = collectMethods(projectDirectory); + Map> callGraph = buildCallGraph(methodMap); + + for (String dbMethod : dbMethods) { + System.out.println("Finding call chains for database method: " + dbMethod); + Set visited = new HashSet<>(); + List callChain = new ArrayList<>(); + traceCallChainsToMethod(dbMethod, callGraph, new HashSet<>(), callChain, methodMap); + } + } + + public void initializeDbMethods() { + dbMethods = new ArrayList<>(); + // Add common database methods by naming convention + addCommonDbMethods(); + + // Scan classes in the project + String packageName = "com.itestra"; // Specify your project's base package + List> classes = getClasses(packageName); + + for (Class clazz : classes) { + // Check if class is a repository + if (clazz.isAnnotationPresent(Repository.class)) { + scanClassForDbMethods(clazz); + } + + // Check if methods are transactional + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Transactional.class)) { + dbMethods.add(method.getName()); + } + } + } + } + + private void addCommonDbMethods() { + dbMethods.add("flush"); + dbMethods.add("save"); + dbMethods.add("update"); + dbMethods.add("persist"); + dbMethods.add("remove"); + dbMethods.add("find"); + dbMethods.add("selectById"); + dbMethods.add("selectExistingById"); + dbMethods.add("delete"); + dbMethods.add("selectByArticleNumber"); + dbMethods.add("saveWithoutFlush"); + dbMethods.add("merge"); + dbMethods.add("selectFrom"); + } + + private void scanClassForDbMethods(Class clazz) { + for (Method method : clazz.getDeclaredMethods()) { + if (isDatabaseMethod(method)) { + dbMethods.add(method.getName()); + } + } + } + + private boolean isDatabaseMethod(Method method) { + String methodName = method.getName().toLowerCase(); + // Check common database operation prefixes + return methodName.startsWith("save") || + methodName.startsWith("find") || + methodName.startsWith("delete") || + methodName.startsWith("update") || + methodName.startsWith("select") || + methodName.startsWith("remove") || + methodName.startsWith("persist") || + methodName.startsWith("flush") || + methodName.startsWith("merge"); + } + + private List> getClasses(String packageName) { + // Use ClassGraph to scan and retrieve all classes in the package + List> classes = new ArrayList<>(); + try (ScanResult scanResult = new ClassGraph().acceptPackages(packageName).scan()) { + for (ClassInfo classInfo : scanResult.getAllClasses()) { + classes.add(Class.forName(classInfo.getName())); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return classes; + } + + public List getDbMethods() { + return dbMethods; + } + + private static Map collectMethods(File projectDirectory) { + Map methodMap = new HashMap<>(); + List javaFiles = getJavaFiles(projectDirectory); + + if (javaFiles != null) { + for (File javaFile : javaFiles) { + try { + String source = new String(Files.readAllBytes(javaFile.toPath())); + ASTParser parser = ASTParser.newParser(AST.JLS_Latest); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + cu.accept(new MethodCollector(methodMap)); + } catch (IOException e) { + System.err.println("Error reading file: " + javaFile.getName()); + e.printStackTrace(); + } + } + } + + return methodMap; + } + + private static List getJavaFiles(File directory) { + List javaFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + javaFiles.addAll(getJavaFiles(file)); + } else if (file.getName().endsWith(".java")) { + javaFiles.add(file); + } + } + } + + return javaFiles; + } + + private static Map> buildCallGraph(Map methodMap) { + Map> callGraph = new HashMap<>(); + + for (MethodDeclaration method : methodMap.values()) { + method.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + String caller = method.getName().toString(); + String callee = node.getName().toString(); + callGraph.computeIfAbsent(caller, k -> new HashSet<>()).add(callee); + return super.visit(node); + } + }); + } + + return callGraph; + } + + private static void traceCallChainsToMethod(String targetMethod, Map> callGraph, Set visited, List callChain, Map methodMap) { + if (!visited.add(targetMethod)) { + return; + } + callChain.add(targetMethod); + Set callees = callGraph.get(targetMethod); + if (callees != null) { + for (String callee : callees) { + List newCallChain = new ArrayList<>(callChain); + traceCallChainsToMethod(callee, callGraph, visited, newCallChain, methodMap); + } + } else { + if (dbMethods.contains(targetMethod)) { + System.out.println("Database method call chain: " + String.join(" -> ", callChain)); + } + } + } + + public static class MethodCollector extends ASTVisitor { + private final Map methodMap; + + public MethodCollector(Map methodMap) { + this.methodMap = methodMap; + } + + @Override + public boolean visit(MethodDeclaration node) { + methodMap.put(node.getName().toString(), node); + return super.visit(node); + } + } +} diff --git a/src/main/java/tum/dpid/NewCallChainAnalyzer.java b/src/main/java/tum/dpid/NewCallChainAnalyzer.java new file mode 100644 index 0000000..8d9f35a --- /dev/null +++ b/src/main/java/tum/dpid/NewCallChainAnalyzer.java @@ -0,0 +1,124 @@ +package tum.dpid; + +import org.eclipse.jdt.core.dom.*; +import java.util.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class NewCallChainAnalyzer { + + private static final List DB_METHODS = List.of("flush", "save", "update", "persist", "remove", "find", "selectById", "selectExistingById", "delete", "selectByArticleNumber", "saveWithoutFlush", "merge", "selectFrom"); + + public static void main(String[] args) { + String projectDirectoryPath = "../fromItestra/LoopAntiPattern"; + + File projectDirectory = new File(projectDirectoryPath); + if (!projectDirectory.exists() || !projectDirectory.isDirectory()) { + System.out.println("Invalid project directory path."); + return; + } + + Map methodMap = collectMethods(projectDirectory); + Map> callGraph = buildCallGraph(methodMap); + + for (String dbMethod : DB_METHODS) { + System.out.println("Finding call chains for database method: " + dbMethod); + Set visited = new HashSet<>(); + List callChain = new ArrayList<>(); + traceCallChainsToMethod(dbMethod, callGraph, new HashSet<>(), callChain, methodMap); + } + } + + private static Map collectMethods(File projectDirectory) { + Map methodMap = new HashMap<>(); + List javaFiles = getJavaFiles(projectDirectory); + + if (javaFiles != null) { + for (File javaFile : javaFiles) { + try { + String source = new String(Files.readAllBytes(javaFile.toPath())); + ASTParser parser = ASTParser.newParser(AST.JLS_Latest); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + cu.accept(new MethodCollector(methodMap)); + } catch (IOException e) { + System.err.println("Error reading file: " + javaFile.getName()); + e.printStackTrace(); + } + } + } + + return methodMap; + } + + private static List getJavaFiles(File directory) { + List javaFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + javaFiles.addAll(getJavaFiles(file)); + } else if (file.getName().endsWith(".java")) { + javaFiles.add(file); + } + } + } + + return javaFiles; + } + + private static Map> buildCallGraph(Map methodMap) { + Map> callGraph = new HashMap<>(); + + for (MethodDeclaration method : methodMap.values()) { + method.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + String caller = method.getName().toString(); + String callee = node.getName().toString(); + callGraph.computeIfAbsent(caller, k -> new HashSet<>()).add(callee); + return super.visit(node); + } + }); + } + + return callGraph; + } + + private static void traceCallChainsToMethod(String targetMethod, Map> callGraph, Set visited, List callChain, Map methodMap) { + if (!visited.add(targetMethod)) { + return; + } + callChain.add(targetMethod); + Set callees = callGraph.get(targetMethod); + if (callees != null) { + for (String callee : callees) { + List newCallChain = new ArrayList<>(callChain); + traceCallChainsToMethod(callee, callGraph, visited, newCallChain, methodMap); + } + } else { + if (DB_METHODS.contains(targetMethod)) { + System.out.println("Database method call chain: " + String.join(" -> ", callChain)); + } + } + } + + static class MethodCollector extends ASTVisitor { + private final Map methodMap; + + MethodCollector(Map methodMap) { + this.methodMap = methodMap; + } + + @Override + public boolean visit(MethodDeclaration node) { + methodMap.put(node.getName().toString(), node); + return super.visit(node); + } + } +} + diff --git a/src/main/java/tum/dpid/OldDynamicCallChainAnalyzer.java b/src/main/java/tum/dpid/OldDynamicCallChainAnalyzer.java new file mode 100644 index 0000000..3456ea5 --- /dev/null +++ b/src/main/java/tum/dpid/OldDynamicCallChainAnalyzer.java @@ -0,0 +1,253 @@ +package tum.dpid; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.util.*; + +import org.eclipse.jdt.core.dom.*; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; + +public class OldDynamicCallChainAnalyzer { + + private static List dbMethods; + + public static void main(String[] args) { + OldDynamicCallChainAnalyzer analyzer = new OldDynamicCallChainAnalyzer(); + analyzer.initializeDbMethods(); + + String projectDirectoryPath = "../fromItestra/LoopAntiPattern"; + + File projectDirectory = new File(projectDirectoryPath); + if (!projectDirectory.exists() || !projectDirectory.isDirectory()) { + System.out.println("Invalid project directory path."); + return; + } + + Map methodMap = collectMethods(projectDirectory); + Map> callGraph = buildCallGraph(methodMap); + + for (String dbMethod : dbMethods) { + System.out.println("Call chain for method: " + dbMethod); + Set visited = new HashSet<>(); + List callChain = new ArrayList<>(); + traceCallChainInOrder(dbMethod, callGraph, new HashSet<>(), callChain, methodMap); + + if (callChain.isEmpty()) { + System.out.println("No calls found for method: " + dbMethod); + } else { + for (String method : callChain) { + System.out.println(method); + } + } + + System.out.println("Call graph for method: " + dbMethod); + drawCallGraph(dbMethod, callGraph, 0, new HashSet<>()); + System.out.println(); + } + } + + public void initializeDbMethods() { + dbMethods = new ArrayList<>(); + // Add common database methods by naming convention + addCommonDbMethods(); + + // Scan classes in the project + String packageName = "com.example.LoopAntiPattern"; // Specify your project's base package + List> classes = getClasses(packageName); + + for (Class clazz : classes) { + // Check if class is a repository + if (clazz.isAnnotationPresent(Repository.class)) { + scanClassForDbMethods(clazz); + } + + // Check if methods are transactional + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Transactional.class)) { + dbMethods.add(method.getName()); + } + } + } + } + + private void addCommonDbMethods() { + dbMethods.add("flush"); + dbMethods.add("save"); + dbMethods.add("update"); + dbMethods.add("persist"); + dbMethods.add("remove"); + dbMethods.add("find"); + dbMethods.add("selectById"); + dbMethods.add("selectExistingById"); + dbMethods.add("delete"); + dbMethods.add("selectByArticleNumber"); + dbMethods.add("saveWithoutFlush"); + dbMethods.add("merge"); + dbMethods.add("selectFrom"); + } + + private void scanClassForDbMethods(Class clazz) { + for (Method method : clazz.getDeclaredMethods()) { + if (isDatabaseMethod(method)) { + dbMethods.add(method.getName()); + } + } + } + + private boolean isDatabaseMethod(Method method) { + String methodName = method.getName().toLowerCase(); + // Check common database operation prefixes + return methodName.startsWith("save") || + methodName.startsWith("find") || + methodName.startsWith("delete") || + methodName.startsWith("update") || + methodName.startsWith("select") || + methodName.startsWith("remove") || + methodName.startsWith("persist") || + methodName.startsWith("flush") || + methodName.startsWith("merge"); + } + + private List> getClasses(String packageName) { + // Use ClassGraph to scan and retrieve all classes in the package + List> classes = new ArrayList<>(); + try (ScanResult scanResult = new ClassGraph().acceptPackages(packageName).scan()) { + for (ClassInfo classInfo : scanResult.getAllClasses()) { + classes.add(Class.forName(classInfo.getName())); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return classes; + } + + private static Map collectMethods(File projectDirectory) { + Map methodMap = new HashMap<>(); + List javaFiles = getJavaFiles(projectDirectory); + + if (javaFiles != null) { + for (File javaFile : javaFiles) { + try { + String source = new String(Files.readAllBytes(javaFile.toPath())); + ASTParser parser = ASTParser.newParser(AST.JLS_Latest); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + cu.accept(new MethodCollector(methodMap)); + } catch (IOException e) { + System.err.println("Error reading file: " + javaFile.getName()); + e.printStackTrace(); + } + } + } + + return methodMap; + } + + private static List getJavaFiles(File directory) { + List javaFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + javaFiles.addAll(getJavaFiles(file)); + } else if (file.getName().endsWith(".java")) { + javaFiles.add(file); + } + } + } + + return javaFiles; + } + + private static Map> buildCallGraph(Map methodMap) { + Map> callGraph = new HashMap<>(); + + for (MethodDeclaration method : methodMap.values()) { + method.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + String caller = method.getName().toString(); + String callee = node.getName().toString(); + callGraph.computeIfAbsent(callee, k -> new HashSet<>()).add(caller); + return super.visit(node); + } + }); + } + + return callGraph; + } + + private static void traceCallChainInOrder(String methodName, Map> callGraph, Set visited, List callChain, Map methodMap) { + if (!visited.add(methodName)) { + return; + } + callChain.add(methodName); + Set callees = callGraph.get(methodName); + if (callees != null) { + for (String callee : callees) { + traceCallChainInOrder(callee, callGraph, visited, callChain, methodMap); + } + } + } + + private static void drawCallGraph(String methodName, Map> callGraph, int level, Set visited) { + if (!visited.add(methodName)) { + return; + } + printIndented(methodName, level); + Set callers = callGraph.get(methodName); + if (callers != null) { + for (String caller : callers) { + drawCallGraph(caller, callGraph, level + 1, visited); + } + } + } + + private static void printIndented(String methodName, int level) { + for (int i = 0; i < level; i++) { + System.out.print(" "); + } + System.out.println(methodName); + } + + private static void traceCallChainsToMethod(String targetMethod, Map> callGraph, Set visited, List callChain, Map methodMap) { + if (!visited.add(targetMethod)) { + return; + } + callChain.add(targetMethod); + Set callees = callGraph.get(targetMethod); + if (callees != null) { + for (String callee : callees) { + List newCallChain = new ArrayList<>(callChain); + traceCallChainsToMethod(callee, callGraph, visited, newCallChain, methodMap); + } + } else { + if (dbMethods.contains(targetMethod)) { + System.out.println("Database method call chain: " + String.join(" -> ", callChain)); + } + } + } + + public static class MethodCollector extends ASTVisitor { + private final Map methodMap; + + public MethodCollector(Map methodMap) { + this.methodMap = methodMap; + } + + @Override + public boolean visit(MethodDeclaration node) { + methodMap.put(node.getName().toString(), node); + return super.visit(node); + } + } +}