Skip to content

Commit bfc1f11

Browse files
authored
Merge pull request #116 from codellm-devkit/113-call-graph-is-missing-edges-to-implementations-of-interfaces
Fix Issue 113: Call graph is missing edges to implementations of interface classes. Merge to main.
2 parents 67a8769 + df259c6 commit bfc1f11

File tree

23 files changed

+789
-193
lines changed

23 files changed

+789
-193
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=2.2.0
1+
version=2.2.1

src/main/java/com/ibm/cldk/SystemDependencyGraph.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import com.ibm.wala.cast.ir.ssa.AstIRFactory;
2121
import com.ibm.wala.cast.java.translator.jdt.ecj.ECJClassLoaderFactory;
2222
import com.ibm.wala.classLoader.CallSiteReference;
23+
import com.ibm.wala.classLoader.JavaLanguage;
24+
import com.ibm.wala.classLoader.Language;
2325
import com.ibm.wala.ipa.callgraph.*;
2426
import com.ibm.wala.ipa.callgraph.AnalysisOptions.ReflectionOptions;
2527
import com.ibm.wala.ipa.callgraph.impl.Util;
@@ -260,8 +262,9 @@ public static List<Dependency> construct(
260262
CallGraph callGraph;
261263
CallGraphBuilder<InstanceKey> builder;
262264
try {
263-
System.setOut(new PrintStream(new NullOutputStream()));
264-
System.setErr(new PrintStream(new NullOutputStream()));
265+
System.setOut(new PrintStream(NullOutputStream.INSTANCE));
266+
System.setErr(new PrintStream(NullOutputStream.INSTANCE));
267+
// builder = Util.makeRTABuilder(new JavaLanguage(), options, cache, cha);
265268
builder = Util.makeRTABuilder(options, cache, cha);
266269
callGraph = builder.makeCallGraph(options, null);
267270
} finally {

src/main/java/com/ibm/cldk/utils/AnalysisUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ public static Iterable<Entrypoint> getEntryPoints(IClassHierarchy cha) {
198198
System.exit(1);
199199
return Stream.empty();
200200
}
201-
}).filter(method -> method.isPublic() || method.isPrivate() || method.isProtected() || method.isStatic()).map(method -> new DefaultEntrypoint(method, cha)).collect(Collectors.toList());
202-
201+
}).map(method -> new DefaultEntrypoint(method, cha)).collect(Collectors.toList());
202+
// We're assuming that all methods are potential entrypoints. May revisit this later if the assumption is incorrect.
203203
Log.info("Registered " + entrypoints.size() + " entrypoints.");
204204
return entrypoints;
205205
}

src/main/java/com/ibm/cldk/utils/BuildProject.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
import java.nio.file.Path;
1111
import java.nio.file.Paths;
1212
import java.text.MessageFormat;
13-
import java.util.AbstractMap;
14-
import java.util.ArrayList;
15-
import java.util.Arrays;
16-
import java.util.List;
13+
import java.util.*;
14+
import java.util.stream.Stream;
1715

1816

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

161159
private static boolean buildProject(String projectPath, String build) {
162-
File pomFile = new File(projectPath, "pom.xml");
160+
File pomFile = new File(String.valueOf(Paths.get(projectPath).toAbsolutePath()), "pom.xml");
163161
if (build == null) {
164162
return true;
165163
} else if (build.equals("auto")) {
166164
if (pomFile.exists()) {
167165
Log.info("Found pom.xml in the project directory. Using Maven to build the project.");
168-
return mavenBuild(projectPath); // Use Maven if pom.xml exists
166+
return mavenBuild(Paths.get(projectPath).toAbsolutePath().toString()); // Use Maven if pom.xml exists
169167
} else {
170168
Log.info("Did not find a pom.xml in the project directory. Using Gradle to build the project.");
171169
return gradleBuild(projectPath); // Otherwise, use Gradle
@@ -211,7 +209,7 @@ public static boolean downloadLibraryDependencies(String projectPath, String pro
211209
// created download dir if it does not exist
212210
String projectRoot = projectRootPom != null ? projectRootPom : projectPath;
213211

214-
File pomFile = new File(projectRoot, "pom.xml");
212+
File pomFile = new File((new File(projectRoot)).getAbsoluteFile(), "pom.xml");
215213
if (pomFile.exists()) {
216214
libDownloadPath = Paths.get(projectPath, "target", LIB_DEPS_DOWNLOAD_DIR).toAbsolutePath();
217215
if (mkLibDepDirs(projectPath))
@@ -231,7 +229,7 @@ public static boolean downloadLibraryDependencies(String projectPath, String pro
231229
));
232230
}
233231
Log.info("Found pom.xml in the project directory. Using Maven to download dependencies.");
234-
String[] mavenCommand = {MAVEN_CMD, "--no-transfer-progress", "-f", Paths.get(projectRoot, "pom.xml").toString(), "dependency:copy-dependencies", "-DoutputDirectory=" + libDownloadPath.toString()};
232+
String[] mavenCommand = {MAVEN_CMD, "--no-transfer-progress", "-f", Paths.get(projectRoot, "pom.xml").toAbsolutePath().toString(), "dependency:copy-dependencies", "-DoutputDirectory=" + libDownloadPath.toString()};
235233
return buildWithTool(mavenCommand);
236234
} else if (new File(projectRoot, "build.gradle").exists() || new File(projectRoot, "build.gradle.kts").exists()) {
237235
libDownloadPath = Paths.get(projectPath, "build", LIB_DEPS_DOWNLOAD_DIR).toAbsolutePath();
@@ -271,8 +269,16 @@ public static void cleanLibraryDependencies() {
271269
if (libDownloadPath != null) {
272270
Log.info("Cleaning up library dependency directory: " + libDownloadPath);
273271
try {
274-
Files.walk(libDownloadPath).filter(Files::isRegularFile).map(Path::toFile).forEach(File::delete);
275-
Files.delete(libDownloadPath);
272+
if (libDownloadPath.toFile().getAbsoluteFile().exists()) {
273+
try (Stream<Path> paths = Files.walk(libDownloadPath)) {
274+
paths.sorted(Comparator.reverseOrder()) // Delete files first, then directories
275+
.map(Path::toFile)
276+
.forEach(file -> {
277+
if (!file.delete())
278+
Log.warn("Failed to delete: " + file.getAbsolutePath());
279+
});
280+
}
281+
}
276282
} catch (IOException e) {
277283
Log.warn("Unable to fully delete library dependency directory: " + e.getMessage());
278284
}

src/main/java/com/ibm/cldk/utils/ProjectDirectoryScanner.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import java.nio.file.Files;
55
import java.nio.file.Path;
66
import java.nio.file.Paths;
7+
import java.util.ArrayList;
78
import java.util.List;
89
import java.util.stream.Collectors;
910
import java.util.stream.Stream;
1011

1112
public class ProjectDirectoryScanner {
1213
public static List<Path> classFilesStream(String projectPath) throws IOException {
13-
Path projectDir = Paths.get(projectPath);
14+
Path projectDir = Paths.get(projectPath).toAbsolutePath();
1415
Log.info("Finding *.class files in " + projectDir);
1516
if (Files.exists(projectDir)) {
1617
try (Stream<Path> paths = Files.walk(projectDir)) {
@@ -37,7 +38,7 @@ public static List<Path> jarFilesStream(String projectPath) throws IOException {
3738
.collect(Collectors.toList());
3839
}
3940
}
40-
return null;
41+
return new ArrayList<>();
4142
}
4243

4344
public static List<Path> sourceFilesStream(String projectPath) throws IOException {

src/test/java/com/ibm/cldk/CodeAnalyzerIntegrationTest.java

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package com.ibm.cldk;
22

3+
import com.google.gson.Gson;
4+
import com.google.gson.JsonArray;
5+
import com.google.gson.JsonElement;
6+
import com.google.gson.JsonObject;
7+
import org.json.JSONArray;
38
import org.junit.jupiter.api.BeforeAll;
49
import org.junit.jupiter.api.Test;
510
import org.junit.jupiter.api.Assertions;
611
import org.testcontainers.containers.BindMode;
712
import org.testcontainers.containers.GenericContainer;
13+
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
814
import org.testcontainers.junit.jupiter.Container;
915
import org.testcontainers.junit.jupiter.Testcontainers;
1016
import org.testcontainers.utility.MountableFile;
@@ -13,7 +19,9 @@
1319
import java.io.FileInputStream;
1420
import java.io.IOException;
1521
import java.nio.file.Paths;
22+
import java.time.Duration;
1623
import java.util.Properties;
24+
import java.util.stream.StreamSupport;
1725

1826

1927
@Testcontainers
@@ -25,7 +33,7 @@ public class CodeAnalyzerIntegrationTest {
2533
*/
2634
static String codeanalyzerVersion;
2735
static final String javaVersion = "17";
28-
36+
static String javaHomePath;
2937
static {
3038
// Build project first
3139
try {
@@ -41,16 +49,14 @@ public class CodeAnalyzerIntegrationTest {
4149
}
4250

4351
@Container
44-
static final GenericContainer<?> container = new GenericContainer<>("openjdk:17-jdk")
52+
static final GenericContainer<?> container = new GenericContainer<>("ubuntu:latest")
4553
.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh"))
4654
.withCommand("-c", "while true; do sleep 1; done")
47-
.withFileSystemBind(
48-
String.valueOf(Paths.get(System.getProperty("user.dir")).resolve("build/libs")),
49-
"/opt/jars",
50-
BindMode.READ_WRITE)
55+
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
5156
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("build/libs")), "/opt/jars")
5257
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-corrupt-test")), "/test-applications/mvnw-corrupt-test")
5358
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/plantsbywebsphere")), "/test-applications/plantsbywebsphere")
59+
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/call-graph-test")), "/test-applications/call-graph-test")
5460
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-working-test")), "/test-applications/mvnw-working-test");
5561

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

71+
public CodeAnalyzerIntegrationTest() throws IOException, InterruptedException {
72+
}
73+
6574
@BeforeAll
6675
static void setUp() {
76+
// Install Java 17 in the base container
77+
try {
78+
container.execInContainer("apt-get", "update");
79+
container.execInContainer("apt-get", "install", "-y", "openjdk-17-jdk");
80+
81+
// Get JAVA_HOME dynamically
82+
var javaHomeResult = container.execInContainer("bash", "-c",
83+
"dirname $(dirname $(readlink -f $(which java)))"
84+
);
85+
javaHomePath = javaHomeResult.getStdout().trim();
86+
Assertions.assertFalse(javaHomePath.isEmpty(), "Failed to determine JAVA_HOME");
87+
88+
} catch (IOException | InterruptedException e) {
89+
throw new RuntimeException(e);
90+
}
91+
92+
93+
// Get the version of the codeanalyzer jar
6794
Properties properties = new Properties();
6895
try (FileInputStream fis = new FileInputStream(
6996
Paths.get(System.getProperty("user.dir"), "gradle.properties").toFile())) {
@@ -92,18 +119,42 @@ void shouldHaveCodeAnalyzerJar() throws Exception {
92119
@Test
93120
void shouldBeAbleToRunCodeAnalyzer() throws Exception {
94121
var runCodeAnalyzerJar = container.execInContainer(
95-
"java",
96-
"-jar",
97-
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
98-
"--help"
99-
);
122+
"bash", "-c",
123+
String.format("export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --help",
124+
javaHomePath, codeanalyzerVersion
125+
));
100126

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

133+
@Test
134+
void callGraphShouldHaveKnownEdges() throws Exception {
135+
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
136+
"bash", "-c",
137+
String.format(
138+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/call-graph-test --analysis-level=2",
139+
javaHomePath, codeanalyzerVersion
140+
)
141+
);
142+
143+
144+
// Read the output JSON
145+
Gson gson = new Gson();
146+
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
147+
JsonArray systemDepGraph = jsonObject.getAsJsonArray("system_dependency_graph");
148+
Assertions.assertTrue(StreamSupport.stream(systemDepGraph.spliterator(), false)
149+
.map(JsonElement::getAsJsonObject)
150+
.anyMatch(entry ->
151+
"CALL_DEP".equals(entry.get("type").getAsString()) &&
152+
"1".equals(entry.get("weight").getAsString()) &&
153+
entry.getAsJsonObject("source").get("signature").getAsString().equals("helloString()") &&
154+
entry.getAsJsonObject("target").get("signature").getAsString().equals("log()")
155+
), "Expected edge not found in the system dependency graph");
156+
}
157+
107158
@Test
108159
void corruptMavenShouldNotBuildWithWrapper() throws IOException, InterruptedException {
109160
// Make executable
@@ -131,42 +182,44 @@ void corruptMavenShouldProduceAnalysisArtifactsWhenMVNCommandIsInPath() throws I
131182

132183
@Test
133184
void corruptMavenShouldNotTerminateWithErrorWhenMavenIsNotPresentUnlessAnalysisLevel2() throws IOException, InterruptedException {
134-
// When javaee level 2, we should get a Runtime Exception
185+
// When analysis level 2, we should get a Runtime Exception
135186
var runCodeAnalyzer = container.execInContainer(
136-
"java",
137-
"-jar",
138-
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
139-
"--input=/test-applications/mvnw-corrupt-test",
140-
"--output=/tmp/",
141-
"--analysis-level=2"
187+
"bash", "-c",
188+
String.format(
189+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/mvnw-corrupt-test --output=/tmp/ --analysis-level=2",
190+
javaHomePath, codeanalyzerVersion
191+
)
142192
);
193+
143194
Assertions.assertEquals(1, runCodeAnalyzer.getExitCode());
144195
Assertions.assertTrue(runCodeAnalyzer.getStderr().contains("java.lang.RuntimeException"));
145196
}
146197

147198
@Test
148199
void shouldBeAbleToGenerateAnalysisArtifactForDaytrader8() throws Exception {
149200
var runCodeAnalyzerOnDaytrader8 = mavenContainer.execInContainer(
150-
"java",
151-
"-jar",
152-
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
153-
"--input=/test-applications/daytrader8",
154-
"--analysis-level=1"
201+
"bash", "-c",
202+
String.format(
203+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/daytrader8 --analysis-level=1",
204+
javaHomePath, codeanalyzerVersion
205+
)
155206
);
207+
156208
Assertions.assertTrue(runCodeAnalyzerOnDaytrader8.getStdout().contains("\"is_entrypoint_class\": true"), "No entry point classes found");
157209
Assertions.assertTrue(runCodeAnalyzerOnDaytrader8.getStdout().contains("\"is_entrypoint\": true"), "No entry point methods found");
158210
}
159211

160212
@Test
161213
void shouldBeAbleToDetectCRUDOperationsAndQueriesForPlantByWebsphere() throws Exception {
162214
var runCodeAnalyzerOnPlantsByWebsphere = container.execInContainer(
163-
"java",
164-
"-jar",
165-
String.format("/opt/jars/codeanalyzer-%s.jar", codeanalyzerVersion),
166-
"--input=/test-applications/plantsbywebsphere",
167-
"--analysis-level=1", "--verbose"
215+
"bash", "-c",
216+
String.format(
217+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/plantsbywebsphere --analysis-level=1 --verbose",
218+
javaHomePath, codeanalyzerVersion
219+
)
168220
);
169221

222+
170223
String output = runCodeAnalyzerOnPlantsByWebsphere.getStdout();
171224

172225
Assertions.assertTrue(output.contains("\"query_type\": \"NAMED\""), "No entry point classes found");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.json

src/test/resources/reference_analysis.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/test/resources/test-applications/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ hs_err_pid*
2929
.gradle/
3030
build/
3131

32+
# Don't ignore Gradle wrapper jar file
33+
!gradle-wrapper.jar
34+
3235
# Ignore Maven target folder
3336
target/
3437

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#
2+
# https://help.github.com/articles/dealing-with-line-endings/
3+
#
4+
# Linux start script should use lf
5+
/gradlew text eol=lf
6+
7+
# These are Windows script files and should use crlf
8+
*.bat text eol=crlf
9+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore Gradle project-specific cache directory
2+
.gradle
3+
4+
# Ignore Gradle build output directory
5+
build
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
plugins {
2+
id 'application'
3+
}
4+
5+
repositories {
6+
mavenCentral()
7+
}
8+
9+
java {
10+
sourceCompatibility = JavaVersion.VERSION_11
11+
targetCompatibility = JavaVersion.VERSION_11
12+
}
13+
14+
if (project.hasProperty('mainClass')) {
15+
mainClassName = project.getProperty('mainClass')
16+
} else {
17+
// use a default
18+
mainClassName =("org.example.User")
19+
}
20+
21+
sourceSets {
22+
main {
23+
java {
24+
srcDirs = ["src/main/java"]
25+
}
26+
resources {
27+
srcDirs = ["src/main/resources"]
28+
}
29+
}
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)