Skip to content

Commit

Permalink
Add complex recursion AST check functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
sarpsahinalp committed May 3, 2024
1 parent 693852e commit 3ad1c7b
Show file tree
Hide file tree
Showing 27 changed files with 988 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@
<artifactId>javaparser-core</artifactId>
<version>3.25.9</version>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-symbol-solver-core</artifactId>
<version>3.25.9</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.5.2</version>
</dependency>
<!-- For testing we use a test framework testing framework -->
<dependency>
<groupId>org.junit.platform</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UnwantedRecursionAssert, Path> {

/**
* 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.
* <p>
* The project source directory gets extracted from the build configuration, and
* a <code>pom.xml</code> or <code>build.gradle</code> 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.,
* <code>de.tum.in.test.api</code>, 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
* <code>assertThatSourcesIn(Path.of("src/main/java")).withinPackage("net.example.test")</code>
* will yield an assert for all source files located at and below the
* relative path <code>src/main/java/net/example/test</code>
*/
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<String> 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<String> errorMessage = RecursionCheck.hasCycle(actual, level, startingMethod, excludedMethods);
errorMessage.ifPresent(unwantedSimpleRecursionMessageForAllJavaFiles -> failWithMessage(
"Wanted recursion not found:" + System.lineSeparator() + unwantedSimpleRecursionMessageForAllJavaFiles)); //$NON-NLS-1$
return this;
}
}


85 changes: 85 additions & 0 deletions src/main/java/de/tum/in/test/api/ast/model/MethodCallGraph.java
Original file line number Diff line number Diff line change
@@ -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<String, DefaultEdge> 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<String, DefaultEdge> extractSubgraph(String startVertex) {
DefaultDirectedGraph<String, DefaultEdge> subgraph = new DefaultDirectedGraph<>(null, graph.getEdgeSupplier(), false);

// Set to keep track of visited vertices
Set<String> visited = new HashSet<>();

// Initialize DepthFirstIterator
Iterator<String> 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<String, DefaultEdge> getGraph() {
return graph;
}
}
Loading

0 comments on commit 3ad1c7b

Please sign in to comment.