Skip to content

Fix Issue 113: Call graph is missing edges to implementations of interface classes. Merge to main. #116

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

Merged
Merged
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
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=2.2.0
version=2.2.1
7 changes: 5 additions & 2 deletions src/main/java/com/ibm/cldk/SystemDependencyGraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.ibm.wala.cast.ir.ssa.AstIRFactory;
import com.ibm.wala.cast.java.translator.jdt.ecj.ECJClassLoaderFactory;
import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.JavaLanguage;
import com.ibm.wala.classLoader.Language;
import com.ibm.wala.ipa.callgraph.*;
import com.ibm.wala.ipa.callgraph.AnalysisOptions.ReflectionOptions;
import com.ibm.wala.ipa.callgraph.impl.Util;
Expand Down Expand Up @@ -260,8 +262,9 @@ public static List<Dependency> construct(
CallGraph callGraph;
CallGraphBuilder<InstanceKey> builder;
try {
System.setOut(new PrintStream(new NullOutputStream()));
System.setErr(new PrintStream(new NullOutputStream()));
System.setOut(new PrintStream(NullOutputStream.INSTANCE));
System.setErr(new PrintStream(NullOutputStream.INSTANCE));
// builder = Util.makeRTABuilder(new JavaLanguage(), options, cache, cha);
builder = Util.makeRTABuilder(options, cache, cha);
callGraph = builder.makeCallGraph(options, null);
} finally {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/ibm/cldk/utils/AnalysisUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ public static Iterable<Entrypoint> getEntryPoints(IClassHierarchy cha) {
System.exit(1);
return Stream.empty();
}
}).filter(method -> method.isPublic() || method.isPrivate() || method.isProtected() || method.isStatic()).map(method -> new DefaultEntrypoint(method, cha)).collect(Collectors.toList());

}).map(method -> new DefaultEntrypoint(method, cha)).collect(Collectors.toList());
// We're assuming that all methods are potential entrypoints. May revisit this later if the assumption is incorrect.
Log.info("Registered " + entrypoints.size() + " entrypoints.");
return entrypoints;
}
Expand Down
26 changes: 16 additions & 10 deletions src/main/java/com/ibm/cldk/utils/BuildProject.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.*;
import java.util.stream.Stream;


import static com.ibm.cldk.utils.ProjectDirectoryScanner.classFilesStream;
Expand Down Expand Up @@ -159,13 +157,13 @@ public static boolean gradleBuild(String projectPath) {
}

private static boolean buildProject(String projectPath, String build) {
File pomFile = new File(projectPath, "pom.xml");
File pomFile = new File(String.valueOf(Paths.get(projectPath).toAbsolutePath()), "pom.xml");
if (build == null) {
return true;
} else if (build.equals("auto")) {
if (pomFile.exists()) {
Log.info("Found pom.xml in the project directory. Using Maven to build the project.");
return mavenBuild(projectPath); // Use Maven if pom.xml exists
return mavenBuild(Paths.get(projectPath).toAbsolutePath().toString()); // Use Maven if pom.xml exists
} else {
Log.info("Did not find a pom.xml in the project directory. Using Gradle to build the project.");
return gradleBuild(projectPath); // Otherwise, use Gradle
Expand Down Expand Up @@ -211,7 +209,7 @@ public static boolean downloadLibraryDependencies(String projectPath, String pro
// created download dir if it does not exist
String projectRoot = projectRootPom != null ? projectRootPom : projectPath;

File pomFile = new File(projectRoot, "pom.xml");
File pomFile = new File((new File(projectRoot)).getAbsoluteFile(), "pom.xml");
if (pomFile.exists()) {
libDownloadPath = Paths.get(projectPath, "target", LIB_DEPS_DOWNLOAD_DIR).toAbsolutePath();
if (mkLibDepDirs(projectPath))
Expand All @@ -231,7 +229,7 @@ public static boolean downloadLibraryDependencies(String projectPath, String pro
));
}
Log.info("Found pom.xml in the project directory. Using Maven to download dependencies.");
String[] mavenCommand = {MAVEN_CMD, "--no-transfer-progress", "-f", Paths.get(projectRoot, "pom.xml").toString(), "dependency:copy-dependencies", "-DoutputDirectory=" + libDownloadPath.toString()};
String[] mavenCommand = {MAVEN_CMD, "--no-transfer-progress", "-f", Paths.get(projectRoot, "pom.xml").toAbsolutePath().toString(), "dependency:copy-dependencies", "-DoutputDirectory=" + libDownloadPath.toString()};
return buildWithTool(mavenCommand);
} else if (new File(projectRoot, "build.gradle").exists() || new File(projectRoot, "build.gradle.kts").exists()) {
libDownloadPath = Paths.get(projectPath, "build", LIB_DEPS_DOWNLOAD_DIR).toAbsolutePath();
Expand Down Expand Up @@ -271,8 +269,16 @@ public static void cleanLibraryDependencies() {
if (libDownloadPath != null) {
Log.info("Cleaning up library dependency directory: " + libDownloadPath);
try {
Files.walk(libDownloadPath).filter(Files::isRegularFile).map(Path::toFile).forEach(File::delete);
Files.delete(libDownloadPath);
if (libDownloadPath.toFile().getAbsoluteFile().exists()) {
try (Stream<Path> paths = Files.walk(libDownloadPath)) {
paths.sorted(Comparator.reverseOrder()) // Delete files first, then directories
.map(Path::toFile)
.forEach(file -> {
if (!file.delete())
Log.warn("Failed to delete: " + file.getAbsolutePath());
});
}
}
} catch (IOException e) {
Log.warn("Unable to fully delete library dependency directory: " + e.getMessage());
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/ibm/cldk/utils/ProjectDirectoryScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ProjectDirectoryScanner {
public static List<Path> classFilesStream(String projectPath) throws IOException {
Path projectDir = Paths.get(projectPath);
Path projectDir = Paths.get(projectPath).toAbsolutePath();
Log.info("Finding *.class files in " + projectDir);
if (Files.exists(projectDir)) {
try (Stream<Path> paths = Files.walk(projectDir)) {
Expand All @@ -37,7 +38,7 @@ public static List<Path> jarFilesStream(String projectPath) throws IOException {
.collect(Collectors.toList());
}
}
return null;
return new ArrayList<>();
}

public static List<Path> sourceFilesStream(String projectPath) throws IOException {
Expand Down
109 changes: 81 additions & 28 deletions src/test/java/com/ibm/cldk/CodeAnalyzerIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.ibm.cldk;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.json.JSONArray;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.MountableFile;
Expand All @@ -13,7 +19,9 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Properties;
import java.util.stream.StreamSupport;


@Testcontainers
Expand All @@ -25,7 +33,7 @@ public class CodeAnalyzerIntegrationTest {
*/
static String codeanalyzerVersion;
static final String javaVersion = "17";

static String javaHomePath;
static {
// Build project first
try {
Expand All @@ -41,16 +49,14 @@ public class CodeAnalyzerIntegrationTest {
}

@Container
static final GenericContainer<?> container = new GenericContainer<>("openjdk:17-jdk")
static final GenericContainer<?> container = new GenericContainer<>("ubuntu:latest")
.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh"))
.withCommand("-c", "while true; do sleep 1; done")
.withFileSystemBind(
String.valueOf(Paths.get(System.getProperty("user.dir")).resolve("build/libs")),
"/opt/jars",
BindMode.READ_WRITE)
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-corrupt-test")), "/test-applications/mvnw-corrupt-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/plantsbywebsphere")), "/test-applications/plantsbywebsphere")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/call-graph-test")), "/test-applications/call-graph-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-working-test")), "/test-applications/mvnw-working-test");

@Container
Expand All @@ -62,8 +68,29 @@ public class CodeAnalyzerIntegrationTest {
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-working-test")), "/test-applications/mvnw-working-test")
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/daytrader8")), "/test-applications/daytrader8");

public CodeAnalyzerIntegrationTest() throws IOException, InterruptedException {
}

@BeforeAll
static void setUp() {
// Install Java 17 in the base container
try {
container.execInContainer("apt-get", "update");
container.execInContainer("apt-get", "install", "-y", "openjdk-17-jdk");

// Get JAVA_HOME dynamically
var javaHomeResult = container.execInContainer("bash", "-c",
"dirname $(dirname $(readlink -f $(which java)))"
);
javaHomePath = javaHomeResult.getStdout().trim();
Assertions.assertFalse(javaHomePath.isEmpty(), "Failed to determine JAVA_HOME");

} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}


// Get the version of the codeanalyzer jar
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(
Paths.get(System.getProperty("user.dir"), "gradle.properties").toFile())) {
Expand Down Expand Up @@ -92,18 +119,42 @@ void shouldHaveCodeAnalyzerJar() throws Exception {
@Test
void shouldBeAbleToRunCodeAnalyzer() throws Exception {
var runCodeAnalyzerJar = container.execInContainer(
"java",
"-jar",
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
"--help"
);
"bash", "-c",
String.format("export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --help",
javaHomePath, codeanalyzerVersion
));

Assertions.assertEquals(0, runCodeAnalyzerJar.getExitCode(),
"Command should execute successfully");
Assertions.assertTrue(runCodeAnalyzerJar.getStdout().length() > 0,
"Should have some output");
}

@Test
void callGraphShouldHaveKnownEdges() throws Exception {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/call-graph-test --analysis-level=2",
javaHomePath, codeanalyzerVersion
)
);


// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonArray systemDepGraph = jsonObject.getAsJsonArray("system_dependency_graph");
Assertions.assertTrue(StreamSupport.stream(systemDepGraph.spliterator(), false)
.map(JsonElement::getAsJsonObject)
.anyMatch(entry ->
"CALL_DEP".equals(entry.get("type").getAsString()) &&
"1".equals(entry.get("weight").getAsString()) &&
entry.getAsJsonObject("source").get("signature").getAsString().equals("helloString()") &&
entry.getAsJsonObject("target").get("signature").getAsString().equals("log()")
), "Expected edge not found in the system dependency graph");
}

@Test
void corruptMavenShouldNotBuildWithWrapper() throws IOException, InterruptedException {
// Make executable
Expand Down Expand Up @@ -131,42 +182,44 @@ void corruptMavenShouldProduceAnalysisArtifactsWhenMVNCommandIsInPath() throws I

@Test
void corruptMavenShouldNotTerminateWithErrorWhenMavenIsNotPresentUnlessAnalysisLevel2() throws IOException, InterruptedException {
// When javaee level 2, we should get a Runtime Exception
// When analysis level 2, we should get a Runtime Exception
var runCodeAnalyzer = container.execInContainer(
"java",
"-jar",
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
"--input=/test-applications/mvnw-corrupt-test",
"--output=/tmp/",
"--analysis-level=2"
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/mvnw-corrupt-test --output=/tmp/ --analysis-level=2",
javaHomePath, codeanalyzerVersion
)
);

Assertions.assertEquals(1, runCodeAnalyzer.getExitCode());
Assertions.assertTrue(runCodeAnalyzer.getStderr().contains("java.lang.RuntimeException"));
}

@Test
void shouldBeAbleToGenerateAnalysisArtifactForDaytrader8() throws Exception {
var runCodeAnalyzerOnDaytrader8 = mavenContainer.execInContainer(
"java",
"-jar",
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
"--input=/test-applications/daytrader8",
"--analysis-level=1"
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/daytrader8 --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);

Assertions.assertTrue(runCodeAnalyzerOnDaytrader8.getStdout().contains("\"is_entrypoint_class\": true"), "No entry point classes found");
Assertions.assertTrue(runCodeAnalyzerOnDaytrader8.getStdout().contains("\"is_entrypoint\": true"), "No entry point methods found");
}

@Test
void shouldBeAbleToDetectCRUDOperationsAndQueriesForPlantByWebsphere() throws Exception {
var runCodeAnalyzerOnPlantsByWebsphere = container.execInContainer(
"java",
"-jar",
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
"--input=/test-applications/plantsbywebsphere",
"--analysis-level=1", "--verbose"
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/plantsbywebsphere --analysis-level=1 --verbose",
javaHomePath, codeanalyzerVersion
)
);


String output = runCodeAnalyzerOnPlantsByWebsphere.getStdout();

Assertions.assertTrue(output.contains("\"query_type\": \"NAMED\""), "No entry point classes found");
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/generated/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.json
1 change: 0 additions & 1 deletion src/test/resources/reference_analysis.json

This file was deleted.

3 changes: 3 additions & 0 deletions src/test/resources/test-applications/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ hs_err_pid*
.gradle/
build/

# Don't ignore Gradle wrapper jar file
!gradle-wrapper.jar

# Ignore Maven target folder
target/

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf

# These are Windows script files and should use crlf
*.bat text eol=crlf

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ignore Gradle project-specific cache directory
.gradle

# Ignore Gradle build output directory
build
30 changes: 30 additions & 0 deletions src/test/resources/test-applications/call-graph-test/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
plugins {
id 'application'
}

repositories {
mavenCentral()
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

if (project.hasProperty('mainClass')) {
mainClassName = project.getProperty('mainClass')
} else {
// use a default
mainClassName =("org.example.User")
}

sourceSets {
main {
java {
srcDirs = ["src/main/java"]
}
resources {
srcDirs = ["src/main/resources"]
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading