From 3ad1c7b23eaf23f25d0816bf1e86cd00f587cbac Mon Sep 17 00:00:00 2001 From: sarps Date: Fri, 3 May 2024 18:16:45 +0200 Subject: [PATCH] Add complex recursion AST check functionality --- pom.xml | 10 ++ .../asserting/UnwantedRecursionAssert.java | 164 ++++++++++++++++++ .../test/api/ast/model/MethodCallGraph.java | 85 +++++++++ .../in/test/api/ast/model/RecursionCheck.java | 134 ++++++++++++++ .../in/test/api/ast/model/VisitorAdapter.java | 46 +++++ .../in/test/integration/AstAssertionTest.java | 78 +++++++++ .../testuser/AstAssertionUser.java | 110 ++++++++++++ .../benchmarkASTRecursion/delete_classes.sh | 14 ++ .../benchmarkASTRecursion/generate_classes.sh | 18 ++ .../recursions/complex/no/Fifth.java | 17 ++ .../recursions/complex/no/First.java | 18 ++ .../recursions/complex/no/Fourth.java | 19 ++ .../recursions/complex/no/Second.java | 17 ++ .../recursions/complex/no/Third.java | 17 ++ .../recursions/complex/yes/A.java | 8 + .../recursions/complex/yes/B.java | 8 + .../recursions/complex/yes/C.java | 8 + .../recursions/dynamicDispatch/Animal.java | 38 ++++ .../ClassWithNoExcludeMethods.java | 16 ++ .../RandomParameterThatShouldBeResolved.java | 14 ++ .../recursions/lambda/Example.java | 21 +++ .../overloaded/no/FactorialCalculator.java | 21 +++ .../overloaded/yes/FactorialCalculator.java | 25 +++ .../overridden/BetterFactorialCalculator.java | 25 +++ .../overridden/FactorialCalculator.java | 20 +++ .../recursions/simple/Recursive.java | 13 ++ .../ClassWithMethodsCallingEachOther.java | 24 +++ 27 files changed, 988 insertions(+) create mode 100644 src/main/java/de/tum/in/test/api/ast/asserting/UnwantedRecursionAssert.java create mode 100644 src/main/java/de/tum/in/test/api/ast/model/MethodCallGraph.java create mode 100644 src/main/java/de/tum/in/test/api/ast/model/RecursionCheck.java create mode 100644 src/main/java/de/tum/in/test/api/ast/model/VisitorAdapter.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/delete_classes.sh create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/generate_classes.sh create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/no/Fifth.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/no/First.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/no/Fourth.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/no/Second.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/no/Third.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/yes/A.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/yes/B.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/complex/yes/C.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/dynamicDispatch/Animal.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/excludeMethods/ClassWithNoExcludeMethods.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/excludeMethods/RandomParameterThatShouldBeResolved.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/lambda/Example.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/no/FactorialCalculator.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/yes/FactorialCalculator.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/BetterFactorialCalculator.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/FactorialCalculator.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/simple/Recursive.java create mode 100644 src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/startingNode/ClassWithMethodsCallingEachOther.java diff --git a/pom.xml b/pom.xml index 59805146..d7330047 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,16 @@ javaparser-core 3.25.9 + + com.github.javaparser + javaparser-symbol-solver-core + 3.25.9 + + + org.jgrapht + jgrapht-core + 1.5.2 + org.junit.platform diff --git a/src/main/java/de/tum/in/test/api/ast/asserting/UnwantedRecursionAssert.java b/src/main/java/de/tum/in/test/api/ast/asserting/UnwantedRecursionAssert.java new file mode 100644 index 00000000..197059f0 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/ast/asserting/UnwantedRecursionAssert.java @@ -0,0 +1,164 @@ +package de.tum.in.test.api.ast.asserting; + +import com.github.javaparser.ParserConfiguration.LanguageLevel; +import de.tum.in.test.api.AresConfiguration; +import de.tum.in.test.api.ast.model.RecursionCheck; +import de.tum.in.test.api.util.ProjectSourcesFinder; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractAssert; + +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.fail; + +/** + * Checks whole Java files for unwanted simple recursion + * + * @author Markus Paulsen + * @version 1.0.0 + * @since 1.12.0 + */ +@API(status = Status.MAINTAINED) +public class UnwantedRecursionAssert extends AbstractAssert { + + /** + * The language level for the Java parser + */ + private final LanguageLevel level; + + /** + * The method to start the recursion check from + */ + private final Method startingMethod; + + /** + * The methods to exclude from the recursion check + */ + private final Method[] excludedMethods; + + private UnwantedRecursionAssert(Path path, LanguageLevel level, Method startingMethod, Method... excludedMethods) { + super(requireNonNull(path), UnwantedRecursionAssert.class); + this.level = level; + this.startingMethod = startingMethod; + this.excludedMethods = excludedMethods; + if (!Files.isDirectory(path)) { + fail("The source directory %s does not exist", path); //$NON-NLS-1$ + } + } + + private UnwantedRecursionAssert(Path path, LanguageLevel level) { + this(path, level, null); + } + + /** + * Creates an unwanted simple recursion assertion object for all project source files. + *

+ * The project source directory gets extracted from the build configuration, and + * a pom.xml or build.gradle in the execution path is + * the default build configuration location. The configuration here is the same + * as the one in the structural tests and uses {@link AresConfiguration}. + * + * @return An unwanted simple recursion assertion object (for chaining) + */ + public static UnwantedRecursionAssert assertThatProjectSources() { + var path = ProjectSourcesFinder.findProjectSourcesPath().orElseThrow(() -> //$NON-NLS-1$ + new AssertionError("Could not find project sources folder." //$NON-NLS-1$ + + " Make sure the build file is configured correctly." //$NON-NLS-1$ + + " If it is not located in the execution folder directly," //$NON-NLS-1$ + + " set the location using AresConfiguration methods.")); //$NON-NLS-1$ + return new UnwantedRecursionAssert(path, null); + } + + /** + * Creates an unwanted simple recursion node assertion object for all source files at and below + * the given directory path. + * + * @param directory Path to a directory under which all files are considered + * @return An unwanted simple recursion assertion object (for chaining) + */ + public static UnwantedRecursionAssert assertThatSourcesIn(Path directory) { + Objects.requireNonNull(directory, "The given source path must not be null."); //$NON-NLS-1$ + return new UnwantedRecursionAssert(directory, null); + } + + /** + * Creates an unwanted simple recursion assertion object for all source files in the given + * package, including all of its sub-packages. + * + * @param packageName Java package name in the form of, e.g., + * de.tum.in.test.api, which is resolved + * relative to the path of this UnwantedNodesAssert. + * @return An unwanted simple recursion assertion object (for chaining) + * @implNote The package is split at "." with the resulting segments being + * interpreted as directory structure. So + * assertThatSourcesIn(Path.of("src/main/java")).withinPackage("net.example.test") + * will yield an assert for all source files located at and below the + * relative path src/main/java/net/example/test + */ + public UnwantedRecursionAssert withinPackage(String packageName) { + Objects.requireNonNull(packageName, "The package name must not be null."); //$NON-NLS-1$ + var newPath = actual.resolve(Path.of("", packageName.split("\\."))); //$NON-NLS-1$ //$NON-NLS-2$ + return new UnwantedRecursionAssert(newPath, level, startingMethod, excludedMethods); + } + + /** + * Configures the language level used by the Java parser + * + * @param level The language level for the Java parser + * @return An unwanted simple recursion assertion object (for chaining) + */ + public UnwantedRecursionAssert withLanguageLevel(LanguageLevel level) { + return new UnwantedRecursionAssert(actual, level, startingMethod, excludedMethods); + } + + /** + * Configures the method to start the recursion check from + * @param node The method to start the recursion check from + * @return An unwanted simple recursion assertion object (for chaining) + */ + public UnwantedRecursionAssert startingWithMethod(Method node) { + return new UnwantedRecursionAssert(actual, level, node, excludedMethods); + } + + public UnwantedRecursionAssert excludeMethods(Method... methods) { + return new UnwantedRecursionAssert(actual, level, startingMethod, methods); + } + + /** + * Verifies that the selected Java files do not contain any recursion. + * + * @return This unwanted simple recursion assertion object (for chaining) + */ + public UnwantedRecursionAssert hasNoRecursion() { + if (level == null) { + failWithMessage("The 'level' is not set. Please use UnwantedNodesAssert.withLanguageLevel(LanguageLevel)."); //$NON-NLS-1$ + } + Optional errorMessage = RecursionCheck.hasNoCycle(actual, level, startingMethod, excludedMethods); + errorMessage.ifPresent(unwantedSimpleRecursionMessageForAllJavaFiles -> failWithMessage( + "Unwanted recursion found in methods:" + System.lineSeparator() + unwantedSimpleRecursionMessageForAllJavaFiles)); //$NON-NLS-1$ + return this; + } + + /** + * Verifies that the selected Java files do contain any recursion. + * + * @return This unwanted simple recursion assertion object (for chaining) + */ + public UnwantedRecursionAssert hasRecursion() { + if (level == null) { + failWithMessage("The 'level' is not set. Please use UnwantedNodesAssert.withLanguageLevel(LanguageLevel)."); //$NON-NLS-1$ + } + Optional errorMessage = RecursionCheck.hasCycle(actual, level, startingMethod, excludedMethods); + errorMessage.ifPresent(unwantedSimpleRecursionMessageForAllJavaFiles -> failWithMessage( + "Wanted recursion not found:" + System.lineSeparator() + unwantedSimpleRecursionMessageForAllJavaFiles)); //$NON-NLS-1$ + return this; + } +} + + diff --git a/src/main/java/de/tum/in/test/api/ast/model/MethodCallGraph.java b/src/main/java/de/tum/in/test/api/ast/model/MethodCallGraph.java new file mode 100644 index 00000000..67ef2d81 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/ast/model/MethodCallGraph.java @@ -0,0 +1,85 @@ +package de.tum.in.test.api.ast.model; + +import com.github.javaparser.ast.CompilationUnit; +import org.apiguardian.api.API; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.traverse.DepthFirstIterator; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import static de.tum.in.test.api.ast.model.RecursionCheck.getParametersOfMethod; + +/** + * Create a graph of method calls from a CompilationUnit + */ +@API(status = API.Status.INTERNAL) +public class MethodCallGraph { + private final Graph graph; + + private final String[] excludedMethodIdentifiers; + + public MethodCallGraph(Method... excludedMethods) { + this.graph = new DefaultDirectedGraph<>(DefaultEdge.class); + this.excludedMethodIdentifiers = new String[excludedMethods.length]; + for (int i = 0; i < excludedMethods.length; i++) { + Method m = excludedMethods[i]; + this.excludedMethodIdentifiers[i] = m != null ? m.getDeclaringClass().getName() + "." + m.getName() + getParametersOfMethod(m) : null; + } + } + + /** + * Create a graph from the given CompilationUnit + * @param cu CompilationUnit to be parsed + */ + public void createGraph(CompilationUnit cu) { + cu.accept(new VisitorAdapter(graph, excludedMethodIdentifiers), null); + } + + /** + * Extract a subgraph from the given graph starting from the given vertex + * @param startVertex Vertex to start the extraction from + * @return Subgraph of the given graph + */ + public Graph extractSubgraph(String startVertex) { + DefaultDirectedGraph subgraph = new DefaultDirectedGraph<>(null, graph.getEdgeSupplier(), false); + + // Set to keep track of visited vertices + Set visited = new HashSet<>(); + + // Initialize DepthFirstIterator + Iterator iterator = new DepthFirstIterator<>(graph, startVertex); + + // Add start vertex to subgraph + subgraph.addVertex(startVertex); + visited.add(startVertex); + + // Iterate through the graph + while (iterator.hasNext()) { + String vertex = iterator.next(); + // Add vertex to subgraph if not already visited + if (!visited.contains(vertex)) { + subgraph.addVertex(vertex); + visited.add(vertex); + } + // Add edges to subgraph + graph.edgesOf(vertex).forEach(edge -> { + String source = graph.getEdgeSource(edge); + String target = graph.getEdgeTarget(edge); + if (visited.contains(source) && visited.contains(target)) { + subgraph.addEdge(source, target); + } + }); + } + + return subgraph; + } + + public Graph getGraph() { + return graph; + } +} \ No newline at end of file diff --git a/src/main/java/de/tum/in/test/api/ast/model/RecursionCheck.java b/src/main/java/de/tum/in/test/api/ast/model/RecursionCheck.java new file mode 100644 index 00000000..ddc93d9b --- /dev/null +++ b/src/main/java/de/tum/in/test/api/ast/model/RecursionCheck.java @@ -0,0 +1,134 @@ +package de.tum.in.test.api.ast.model; + +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.symbolsolver.JavaSymbolSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import com.github.javaparser.utils.SourceRoot; +import org.apiguardian.api.API; +import org.jgrapht.Graph; +import org.jgrapht.alg.cycle.CycleDetector; +import org.jgrapht.graph.DefaultEdge; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@API(status = API.Status.INTERNAL) +public class RecursionCheck { + + private RecursionCheck() { + // Hide the implicit public constructor + throw new IllegalStateException(); + } + + /** + * Check if the startingNode has a recursive call + * + * @param pathToSrcRoot Path to the source root + * @param level JavaParser Language Level + * @param startingNode Method to start the recursion check from, which may be {@code null} + * @return Optional.empty() if recursive call is detected, otherwise an error message + */ + public static Optional hasCycle(Path pathToSrcRoot, ParserConfiguration.LanguageLevel level, Method startingNode, Method... excludedMethods) { + MethodCallGraph graph = createMethodCallGraph(pathToSrcRoot, level, excludedMethods); + return !checkCycle(graph, startingNode).isEmpty() ? Optional.empty() : Optional.of("No recursive call detected"); + } + + /** + * Check if the startingNode has no recursive call + * + * @param pathToSrcRoot Path to the source root + * @param level JavaParser Language Level + * @param startingNode Method to start the recursion check from, which may be {@code null} + * @return Optional.empty() if no recursive call is detected, otherwise an error message with methods in the detected cycle + */ + public static Optional hasNoCycle(Path pathToSrcRoot, ParserConfiguration.LanguageLevel level, Method startingNode, Method... excludedMethods) { + MethodCallGraph graph = createMethodCallGraph(pathToSrcRoot, level, excludedMethods); + return checkCycle(graph, startingNode).stream().reduce((s1, s2) -> String.join(", ", s1, s2)); + } + + /** + * Check if the graph has a cycle + * + * @param graph Method call graph + * @param startingNode Method to start the recursion check from, which may be {@code null} + * @return Set of methods in the detected cycle + */ + private static Set checkCycle(MethodCallGraph graph, Method startingNode) { + // Convert Method to Node name + String nodeName = startingNode != null ? startingNode.getDeclaringClass().getName() + "." + startingNode.getName() + getParametersOfMethod(startingNode) : null; + + if (nodeName != null) { + Graph subgraph = graph.extractSubgraph(nodeName); + return new CycleDetector<>(subgraph).findCycles(); + } else { + return new CycleDetector<>(graph.getGraph()).findCycles(); + } + } + + /** + * Get the parameters of the method + * + * @param method Method + * @return String representation of the parameters + */ + public static String getParametersOfMethod(Method method) { + return "(" + Arrays.stream(method.getParameterTypes()).map(Class::getCanonicalName).collect(Collectors.joining(", ")) + ")"; + } + + /** + * Create a method call graph from the source root + * + * @param pathToSrcRoot Path to the source root + * @param level JavaParser Language Level + * @return Method call graph + */ + public static MethodCallGraph createMethodCallGraph(Path pathToSrcRoot, ParserConfiguration.LanguageLevel level, Method... excludedMethods) { + MethodCallGraph methodCallGraph = new MethodCallGraph(excludedMethods); + List> asts = parseFromSourceRoot(pathToSrcRoot, level); + for (Optional ast : asts) { + ast.ifPresent(methodCallGraph::createGraph); + } + + return methodCallGraph; + } + + /** + * Parse all Java files in the source root + * + * @param pathToSourceRoot Path to the source root + * @param level JavaParser Language Level + * @return List of CompilationUnit + */ + public static List> parseFromSourceRoot(Path pathToSourceRoot, ParserConfiguration.LanguageLevel level) { + CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver(); + combinedTypeSolver.add(new ReflectionTypeSolver()); + combinedTypeSolver.add(new JavaParserTypeSolver(new File(pathToSourceRoot.toString()))); + combinedTypeSolver.add(new ClassLoaderTypeSolver(MethodCallGraph.class.getClassLoader())); + + // Configure JavaParser to use type resolution + JavaSymbolSolver symbolSolver = new JavaSymbolSolver(combinedTypeSolver); + + SourceRoot sourceRoot = new SourceRoot(pathToSourceRoot, new ParserConfiguration().setSymbolResolver(symbolSolver).setLanguageLevel(level)); + + List> parseResults; + try { + parseResults = sourceRoot.tryToParse(); + } catch (IOException e) { + throw new AssertionError(String.format("The file %s could not be read:", e)); + } + + return parseResults.stream().map(ParseResult::getResult).collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/tum/in/test/api/ast/model/VisitorAdapter.java b/src/main/java/de/tum/in/test/api/ast/model/VisitorAdapter.java new file mode 100644 index 00000000..cf3da082 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/ast/model/VisitorAdapter.java @@ -0,0 +1,46 @@ +package de.tum.in.test.api.ast.model; + +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; + +public class VisitorAdapter extends VoidVisitorAdapter { + private final Graph graph; + + private final String[] excludedMethodIdentifiers; + + public VisitorAdapter(Graph graph, String... excludedMethodIdentifiers) { + this.graph = graph; + this.excludedMethodIdentifiers = excludedMethodIdentifiers; + } + + @Override + public void visit(MethodDeclaration md, Void arg) { + super.visit(md, arg); + String vertexName = md.resolve().getQualifiedSignature(); + if (isExcluded(vertexName)) { + return; + } + graph.addVertex(vertexName); + md.findAll(MethodCallExpr.class).forEach(mce -> { + String calleeVertexName = mce.resolve().getQualifiedSignature(); + graph.addVertex(calleeVertexName); + graph.addEdge(vertexName, calleeVertexName); + }); + } + /** + * Check if the given vertex is excluded from the cycle check + * @param vertex Vertex to check + * @return True if the vertex is excluded, false otherwise + */ + private boolean isExcluded(String vertex) { + for (String excludedIdentifier : excludedMethodIdentifiers) { + if (vertex.equals(excludedIdentifier)) { + return true; + } + } + return false; + } +} diff --git a/src/test/java/de/tum/in/test/integration/AstAssertionTest.java b/src/test/java/de/tum/in/test/integration/AstAssertionTest.java index d2545f17..18571e11 100644 --- a/src/test/java/de/tum/in/test/integration/AstAssertionTest.java +++ b/src/test/java/de/tum/in/test/integration/AstAssertionTest.java @@ -638,4 +638,82 @@ void test_testLevelIsNull() { "The 'level' is not set. Please use UnwantedNodesAssert.withLanguageLevel(LanguageLevel).")); } } + + @Nested + @DisplayName("Recursion-Test-Tests") + class RecursionTestTests { + + @TestTest + void test_testExcludesPassedMethods_Success() { + String testExcludesPassedMethods_Success = "testExcludesPassedMethods_Success"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testExcludesPassedMethods_Success)); + } + + @TestTest + void test_testExcludesPassedMethod_Fail() { + String testExcludesPassedMethod_Fail = "testExcludesPassedMethod_Fail"; + tests.assertThatEvents().haveExactly(1, testFailedWith(testExcludesPassedMethod_Fail, AssertionError.class, "Unwanted recursion found in methods:\n" + + "de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.excludeMethods.ClassWithNoExcludeMethods.something(de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.excludeMethods.RandomParameterThatShouldBeResolved)")); + } + + @TestTest + void test_testShouldDetectRecursionGivenStartingNode() { + String testShouldDetectRecursionGivenStartingNode = "testShouldDetectRecursionGivenStartingNode"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testShouldDetectRecursionGivenStartingNode)); + } + + @TestTest + void test_testShouldNotDetectRecursionGivenStartingNode() { + String testShouldNotDetectRecursionGivenStartingNode = "testShouldNotDetectRecursionGivenStartingNode"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testShouldNotDetectRecursionGivenStartingNode)); + } + + @TestTest + void test_testDetectComplexNoRecursion() { + String testDetectComplexNoRecursion = "testDetectComplexNoRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectComplexNoRecursion)); + } + + @TestTest + void test_testDetectComplexRecursion() { + String testDetectComplexRecursion = "testDetectComplexRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectComplexRecursion)); + } + + @TestTest + void test_testDetectDynamicDispatchRecursion() { + String testDetectDynamicDispatchRecursion = "testDetectDynamicDispatchRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectDynamicDispatchRecursion)); + } + + @TestTest + void test_testDetectOverloadedRecursion() { + String testDetectOverloadedRecursion = "testDetectOverloadedRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectOverloadedRecursion)); + } + + @TestTest + void test_testDetectOverloadedNoRecursion() { + String testDetectOverloadedNoRecursion = "testDetectOverloadedNoRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectOverloadedNoRecursion)); + } + + @TestTest + void test_testDetectOverriddenRecursion() { + String testDetectOverriddenRecursion = "testDetectOverriddenRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectOverriddenRecursion)); + } + + @TestTest + void test_testDetectSimpleRecursion() { + String testDetectSimpleRecursion = "testDetectSimpleRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectSimpleRecursion)); + } + + @TestTest + void test_testDetectLambdaRecursion() { + String testDetectLambdaRecursion = "testDetectLambdaRecursion"; + tests.assertThatEvents().haveExactly(1, finishedSuccessfully(testDetectLambdaRecursion)); + } + } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/AstAssertionUser.java b/src/test/java/de/tum/in/test/integration/testuser/AstAssertionUser.java index 599836a6..a94f0b14 100644 --- a/src/test/java/de/tum/in/test/integration/testuser/AstAssertionUser.java +++ b/src/test/java/de/tum/in/test/integration/testuser/AstAssertionUser.java @@ -3,6 +3,11 @@ import java.io.IOException; import java.nio.file.Path; +import de.tum.in.test.api.ast.asserting.UnwantedRecursionAssert; +import de.tum.in.test.api.util.ReflectionTestUtils; +import de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.excludeMethods.ClassWithNoExcludeMethods; +import de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.excludeMethods.RandomParameterThatShouldBeResolved; +import de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.startingNode.ClassWithMethodsCallingEachOther; import org.junit.jupiter.api.*; import com.github.javaparser.ParserConfiguration; @@ -436,4 +441,109 @@ void testLevelIsNull() { UnwantedNodesAssert.assertThatProjectSources().hasNo(LoopType.ANY); } } + + @Nested + @DisplayName("Recursion-Tests") + class RecursionTests { + @Test + void testExcludesPassedMethods_Success() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.excludeMethods") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .excludeMethods(ReflectionTestUtils.getMethod(ClassWithNoExcludeMethods.class, "something", RandomParameterThatShouldBeResolved.class)) + .hasNoRecursion(); + } + + @Test + void testExcludesPassedMethod_Fail() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.excludeMethods") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .excludeMethods(ReflectionTestUtils.getMethod(ClassWithNoExcludeMethods.class, "main", String[].class)) + .hasNoRecursion(); + } + + @Test + void testShouldDetectRecursionGivenStartingNode() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.startingNode") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .startingWithMethod(ReflectionTestUtils.getMethod(ClassWithMethodsCallingEachOther.class, "main", String[].class)) + .hasRecursion(); + } + + @Test + void testShouldNotDetectRecursionGivenStartingNode() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.startingNode") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .startingWithMethod(ReflectionTestUtils.getMethod(ClassWithMethodsCallingEachOther.class, "method3")) + .hasNoRecursion(); + } + + @Test + void testDetectComplexNoRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.complex.no") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasNoRecursion(); + } + + @Test + void testDetectComplexRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.complex.yes") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasRecursion(); + } + + @Test + void testDetectDynamicDispatchRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.dynamicDispatch") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasRecursion(); + } + + @Test + void testDetectOverloadedRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.overloaded.yes") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasRecursion(); + } + + @Test + void testDetectOverloadedNoRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.overloaded.no") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .excludeMethods(ReflectionTestUtils.getMethod(ClassWithNoExcludeMethods.class, "something", RandomParameterThatShouldBeResolved.class)) + .hasNoRecursion(); + } + + @Test + void testDetectOverriddenRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.overridden") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasRecursion(); + } + + @Test + void testDetectSimpleRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.simple") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasRecursion(); + } + + @Test + void testDetectLambdaRecursion() { + UnwantedRecursionAssert.assertThatProjectSources() + .withinPackage(BASE_PACKAGE + ".recursions.lambda") + .withLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17) + .hasRecursion(); + } + } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/delete_classes.sh b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/delete_classes.sh new file mode 100644 index 00000000..e52e7cfa --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/delete_classes.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Loop to delete 10000 Java classes +for ((i=1; i<=10000; i++)) +do + # Define the class name + class_name="TestClass$i.java" + + # Check if the file exists before attempting to delete + if [ -f "$class_name" ]; then + rm "$class_name" + echo "Deleted: $class_name" + fi +done diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/generate_classes.sh b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/generate_classes.sh new file mode 100644 index 00000000..c964f294 --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/benchmarkASTRecursion/generate_classes.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Loop to create 10000 Java classes +for ((i=1; i<=10000; i++)) +do + # Define the class name + class_name="TestClass$i" + + # Create the Java file with the class definition + cat > "$class_name.java" < { + method2(); + }; + r.run(); + } + + public void method2() { + Runnable r = () -> { + method(); + }; + r.run(); + } + + +} diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/no/FactorialCalculator.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/no/FactorialCalculator.java new file mode 100644 index 00000000..ea7686ed --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/no/FactorialCalculator.java @@ -0,0 +1,21 @@ +package de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.overloaded.no; + +public class FactorialCalculator { + + public int factorial(int n) { + if (n == 0 || n == 1) { + return 1; + } else { + return (int) (n * factorial((double) n - 1)); + } + } + + // Overloaded method for factorial calculation with double input + public double factorial(double n) { + if (n == 0 || n == 1) { + return 1; + } else { + return n; + } + } +} diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/yes/FactorialCalculator.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/yes/FactorialCalculator.java new file mode 100644 index 00000000..5fa3f0cf --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overloaded/yes/FactorialCalculator.java @@ -0,0 +1,25 @@ +package de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.overloaded.yes; + +public class FactorialCalculator { + + // Overloaded method for factorial calculation with integer input + public int factorial(int n) { + if (n == 0 || n == 1) { + return 1; + } else { + // Call the double factorial method with the same argument + return (int) factorial((double) n); + } + } + + // Overloaded method for factorial calculation with double input + public double factorial(double n) { + if (n == 0 || n == 1) { + return 1; + } else { + // Call the integer factorial method with the truncated value of n + return n * factorial((int) n - 1); + } + } +} + diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/BetterFactorialCalculator.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/BetterFactorialCalculator.java new file mode 100644 index 00000000..bb1636a6 --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/BetterFactorialCalculator.java @@ -0,0 +1,25 @@ +package de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.overridden; + +public class BetterFactorialCalculator extends FactorialCalculator { + + // Override the calculateFactorial method for better efficiency + @Override + public int calculateFactorial(int n) { + return calculateFactorialHelper(n, 1); + } + + // Helper method with tail recursion for better efficiency + private int calculateFactorialHelper(int n, int resultSoFar) { + if (n == 0 || n == 1) { + return resultSoFar; + } else { + return calculateFactorialHelper(n - 1, n * resultSoFar); + } + } + + public static void main(String[] args) { + BetterFactorialCalculator calculator = new BetterFactorialCalculator(); + int result = calculator.calculateFactorial(5); + System.out.println("Factorial of 5 (better implementation): " + result); + } +} diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/FactorialCalculator.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/FactorialCalculator.java new file mode 100644 index 00000000..1a7f4e58 --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/overridden/FactorialCalculator.java @@ -0,0 +1,20 @@ +package de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.overridden; + +public class FactorialCalculator { + + // Method for calculating factorial (recursive) + public int calculateFactorial(int n) { + if (n == 0 || n == 1) { + return 1; + } else { + return n * calculateFactorial(n - 1); + } + } + + public static void main(String[] args) { + FactorialCalculator calculator = new FactorialCalculator(); + int result = calculator.calculateFactorial(5); + System.out.println("Factorial of 5: " + result); + } +} + diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/simple/Recursive.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/simple/Recursive.java new file mode 100644 index 00000000..db7772b7 --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/simple/Recursive.java @@ -0,0 +1,13 @@ +package de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.simple; + +public class Recursive { + + public void recursiveMethod() { + recursiveMethod(); + } + + public static void main(String[] args) { + Recursive recursive = new Recursive(); + recursive.recursiveMethod(); + } +} diff --git a/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/startingNode/ClassWithMethodsCallingEachOther.java b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/startingNode/ClassWithMethodsCallingEachOther.java new file mode 100644 index 00000000..82545002 --- /dev/null +++ b/src/test/java/de/tum/in/test/integration/testuser/subject/structural/astTestFiles/recursions/startingNode/ClassWithMethodsCallingEachOther.java @@ -0,0 +1,24 @@ +package de.tum.in.test.integration.testuser.subject.structural.astTestFiles.recursions.startingNode; + +public class ClassWithMethodsCallingEachOther { + + public void method1() { + method2(); + } + + public void method2() { + method1(); + } + + public static void main(String[] args) { + new ClassWithMethodsCallingEachOther().method1(); + } + + public void method3() { + method4(); + } + + public void method4() { + System.out.println(); + } +}