Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add complex recursion AST check functionality #370

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
sarpsahinalp marked this conversation as resolved.
Show resolved Hide resolved
</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
Loading