Skip to content

Commit

Permalink
Merge pull request #116 from codellm-devkit/113-call-graph-is-missing…
Browse files Browse the repository at this point in the history
…-edges-to-implementations-of-interfaces

Fix Issue 113: Call graph is missing edges to implementations of interface classes. Merge to main.
  • Loading branch information
rahlk authored Feb 17, 2025
2 parents 67a8769 + df259c6 commit bfc1f11
Show file tree
Hide file tree
Showing 23 changed files with 789 additions and 193 deletions.
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

0 comments on commit bfc1f11

Please sign in to comment.