Skip to content

Commit 4b88b85

Browse files
authored
Merge pull request #121 from codellm-devkit/118-support-analysis-of-java-14-record-classes
Address Issue 118: Support Record Declarations
2 parents 97d2bfe + ae845cd commit 4b88b85

File tree

21 files changed

+670
-17
lines changed

21 files changed

+670
-17
lines changed

build.gradle

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ plugins {
1616
id 'eclipse'
1717
id 'application'
1818
id 'org.graalvm.buildtools.native' version '0.10.4'
19+
id 'org.jetbrains.kotlin.jvm'
1920
}
2021

2122
// Get the version from the property file first
@@ -30,8 +31,6 @@ repositories {
3031
}
3132

3233
java {
33-
sourceCompatibility = JavaVersion.VERSION_11
34-
targetCompatibility = JavaVersion.VERSION_11
3534
}
3635

3736
if (project.hasProperty('mainClass')) {
@@ -121,7 +120,8 @@ dependencies {
121120
implementation('org.jgrapht:jgrapht-core:1.5.2')
122121
implementation('org.jgrapht:jgrapht-io:1.5.2')
123122
implementation('org.jgrapht:jgrapht-ext:1.5.2')
124-
implementation('com.github.javaparser:javaparser-symbol-solver-core:3.25.9')
123+
implementation('com.github.javaparser:javaparser-symbol-solver-core:3.26.3')
124+
implementation('com.github.javaparser:javaparser-core:3.26.3')
125125

126126
// TestContainers
127127
testImplementation 'org.testcontainers:testcontainers:1.19.3'
@@ -135,6 +135,7 @@ dependencies {
135135
// SLF4J - for TestContainers logging
136136
testImplementation 'org.slf4j:slf4j-api:2.0.9'
137137
testImplementation 'org.slf4j:slf4j-simple:2.0.9'
138+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
138139

139140
}
140141

@@ -277,3 +278,6 @@ tasks.register('bumpVersion') {
277278
}
278279

279280
nativeCompile.finalizedBy copyNativeExecutable
281+
kotlin {
282+
jvmToolchain(11)
283+
}

gradle.properties

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

settings.gradle

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
/*
1+
pluginManagement {
2+
plugins {
3+
id 'org.jetbrains.kotlin.jvm' version '2.1.10'
4+
}
5+
}
6+
plugins {
7+
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
8+
}/*
29
Copyright IBM Corporation 2023, 2024
310
411
Licensed under the Apache Public License 2.0, Version 2.0 (the "License");

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

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.ibm.cldk.entities.*;
3434
import com.ibm.cldk.utils.Log;
3535
import org.apache.commons.lang3.tuple.Pair;
36+
import org.checkerframework.checker.units.qual.C;
3637

3738
import java.io.IOException;
3839
import java.nio.file.Path;
@@ -110,8 +111,7 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
110111
cUnit.setTypeDeclarations(parseResult.findAll(TypeDeclaration.class).stream().filter(typeDecl -> typeDecl.getFullyQualifiedName().isPresent()).map(typeDecl -> {
111112
// get type name and initialize the type object
112113
String typeName = typeDecl.getFullyQualifiedName().get().toString();
113-
com.ibm.cldk.entities.Type typeNode = new com.ibm.cldk.entities.Type();
114-
114+
com.ibm.cldk.entities.Type typeNode = new com.ibm.cldk.entities.Type();;
115115
if (typeDecl instanceof ClassOrInterfaceDeclaration) {
116116
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) typeDecl;
117117

@@ -146,8 +146,26 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
146146

147147
// Add enum constants
148148
typeNode.setEnumConstants(enumDecl.getEntries().stream().map(SymbolTable::processEnumConstantDeclaration).collect(Collectors.toList()));
149+
}
150+
else if (typeDecl instanceof RecordDeclaration) {
151+
RecordDeclaration recordDecl = (RecordDeclaration) typeDecl;
149152

150-
} else {
153+
// Set that this is a record declaration
154+
typeNode.setRecordDeclaration(typeDecl.isRecordDeclaration());
155+
156+
// Add interfaces implemented by record
157+
typeNode.setImplementsList(recordDecl.getImplementedTypes().stream().map(SymbolTable::resolveType).collect(Collectors.toList()));
158+
159+
// Add record modifiers
160+
typeNode.setModifiers(recordDecl.getModifiers().stream().map(m -> m.toString().strip()).collect(Collectors.toList()));
161+
162+
// Add record annotations
163+
typeNode.setAnnotations(recordDecl.getAnnotations().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
164+
165+
// Add record components
166+
typeNode.setRecordComponents(processRecordComponents(recordDecl));
167+
}
168+
else {
151169
// TODO: handle AnnotationDeclaration, RecordDeclaration
152170
// set the common type attributes only
153171
Log.warn("Found unsupported type declaration: " + typeDecl.toString());
@@ -162,7 +180,6 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
162180
typeNode.setClassOrInterfaceDeclaration(typeDecl.isClassOrInterfaceDeclaration());
163181
typeNode.setEnumDeclaration(typeDecl.isEnumDeclaration());
164182
typeNode.setAnnotationDeclaration(typeDecl.isAnnotationDeclaration());
165-
typeNode.setRecordDeclaration(typeDecl.isRecordDeclaration());
166183

167184
// Add class comment
168185
typeNode.setComment(typeDecl.getComment().isPresent() ? typeDecl.getComment().get().asString() : "");
@@ -196,6 +213,40 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
196213
return cUnit;
197214
}
198215

216+
217+
private static List<RecordComponent> processRecordComponents(RecordDeclaration recordDecl) {
218+
return recordDecl.getParameters().stream().map(
219+
parameter -> {
220+
RecordComponent recordComponent = new RecordComponent();
221+
recordComponent.setName(parameter.getNameAsString());
222+
recordComponent.setType(resolveType(parameter.getType()));
223+
recordComponent.setAnnotations(parameter.getAnnotations().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
224+
recordComponent.setModifiers(parameter.getModifiers().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
225+
recordComponent.setVarArgs(parameter.isVarArgs());
226+
recordComponent.setDefaultValue(mapRecordConstructorDefaults(recordDecl).getOrDefault(parameter.getNameAsString(), null));
227+
return recordComponent;
228+
}
229+
).collect(Collectors.toList());
230+
}
231+
232+
private static Map<String, Object> mapRecordConstructorDefaults(RecordDeclaration recordDecl) {
233+
234+
return recordDecl.getCompactConstructors().stream()
235+
.flatMap(constructor -> constructor.findAll(AssignExpr.class).stream()) // Flatten all assignments
236+
.filter(assignExpr -> assignExpr.getTarget().isNameExpr()) // Ensure assignment is to a parameter
237+
.collect(Collectors.toMap(
238+
assignExpr -> assignExpr.getTarget().asNameExpr().getNameAsString(), // Key: Parameter Name
239+
assignExpr -> Optional.ofNullable(assignExpr.getValue()).map(valueExpr -> { // Value: Default Value
240+
return valueExpr.isStringLiteralExpr() ? valueExpr.asStringLiteralExpr().asString()
241+
: valueExpr.isBooleanLiteralExpr() ? valueExpr.asBooleanLiteralExpr().getValue()
242+
: valueExpr.isCharLiteralExpr() ? valueExpr.asCharLiteralExpr().getValue()
243+
: valueExpr.isDoubleLiteralExpr() ? valueExpr.asDoubleLiteralExpr().asDouble()
244+
: valueExpr.isIntegerLiteralExpr() ? valueExpr.asIntegerLiteralExpr().asNumber()
245+
: valueExpr.isLongLiteralExpr() ? valueExpr.asLongLiteralExpr().asNumber()
246+
: valueExpr.isNullLiteralExpr() ? null
247+
: valueExpr.toString();}).orElse("null"))); // Default: store as a string
248+
}
249+
199250
private static boolean isEntryPointClass(TypeDeclaration typeDecl) {
200251
return isSpringEntrypointClass(typeDecl) || isStrutsEntryPointClass(typeDecl) || isCamelEntryPointClass(typeDecl) || isJaxRSEntrypointClass(typeDecl) || isJakartaServletEntryPointClass(typeDecl);
201252

@@ -904,7 +955,7 @@ private static String resolveType(Type type) {
904955
public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>> extractAll(Path projectRootPath) throws IOException {
905956
SymbolSolverCollectionStrategy symbolSolverCollectionStrategy = new SymbolSolverCollectionStrategy();
906957
ProjectRoot projectRoot = symbolSolverCollectionStrategy.collect(projectRootPath);
907-
javaSymbolSolver = (JavaSymbolSolver) symbolSolverCollectionStrategy.getParserConfiguration().getSymbolResolver().get();
958+
javaSymbolSolver = (JavaSymbolSolver) symbolSolverCollectionStrategy.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21).getSymbolResolver().get();
908959
Map<String, JavaCompilationUnit> symbolTable = new LinkedHashMap<>();
909960
Map<String, List<Problem>> parseProblems = new HashMap<>();
910961
for (SourceRoot sourceRoot : projectRoot.getSourceRoots()) {
@@ -927,7 +978,7 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
927978
CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver();
928979
combinedTypeSolver.add(new ReflectionTypeSolver());
929980

930-
ParserConfiguration parserConfiguration = new ParserConfiguration();
981+
ParserConfiguration parserConfiguration = new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21);
931982
parserConfiguration.setSymbolResolver(new JavaSymbolSolver(combinedTypeSolver));
932983

933984
JavaParser javaParser = new JavaParser(parserConfiguration);
@@ -958,7 +1009,8 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
9581009
SymbolSolverCollectionStrategy symbolSolverCollectionStrategy = new SymbolSolverCollectionStrategy();
9591010
ProjectRoot projectRoot = symbolSolverCollectionStrategy.collect(projectRootPath);
9601011
javaSymbolSolver = (JavaSymbolSolver) symbolSolverCollectionStrategy.getParserConfiguration().getSymbolResolver().get();
961-
ParserConfiguration parserConfiguration = new ParserConfiguration();
1012+
Log.info("Setting parser language level to JAVA_21");
1013+
ParserConfiguration parserConfiguration = new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21);
9621014
parserConfiguration.setSymbolResolver(javaSymbolSolver);
9631015

9641016
// create java parser with the configuration
@@ -972,6 +1024,7 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
9721024
ParseResult<CompilationUnit> parseResult = javaParser.parse(javaFilePath);
9731025
if (parseResult.isSuccessful()) {
9741026
CompilationUnit compilationUnit = parseResult.getResult().get();
1027+
System.out.println("Successfully parsed file: " + javaFilePath.toString());
9751028
symbolTable.put(compilationUnit.getStorage().get().getPath().toString(), processCompilationUnit(compilationUnit));
9761029
} else {
9771030
Log.error(parseResult.getProblems().toString());
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ibm.cldk.entities;
2+
3+
import lombok.Data;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
@Data
9+
public class RecordComponent {
10+
private String comment;
11+
private String name;
12+
private String type;
13+
private List<String> modifiers;
14+
private List<String> annotations = new ArrayList<>();
15+
private Object defaultValue = null; // We will store the string representation of the default value
16+
private boolean isVarArgs = false;
17+
}

src/main/java/com/ibm/cldk/entities/Type.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ public class Type {
2727
private Map<String, Callable> callableDeclarations = new HashMap<>();
2828
private List<Field> fieldDeclarations = new ArrayList<>();
2929
private List<EnumConstant> enumConstants = new ArrayList<>();
30+
private List<RecordComponent> recordComponents = new ArrayList<>();
3031
private boolean isEntrypointClass = false;
3132
}

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
import com.google.gson.JsonArray;
55
import com.google.gson.JsonElement;
66
import com.google.gson.JsonObject;
7-
import org.json.JSONArray;
87
import org.junit.jupiter.api.BeforeAll;
98
import org.junit.jupiter.api.Test;
109
import org.junit.jupiter.api.Assertions;
11-
import org.testcontainers.containers.BindMode;
1210
import org.testcontainers.containers.GenericContainer;
13-
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
1411
import org.testcontainers.junit.jupiter.Container;
1512
import org.testcontainers.junit.jupiter.Testcontainers;
1613
import org.testcontainers.utility.MountableFile;
@@ -19,7 +16,7 @@
1916
import java.io.FileInputStream;
2017
import java.io.IOException;
2118
import java.nio.file.Paths;
22-
import java.time.Duration;
19+
import java.util.Map;
2320
import java.util.Properties;
2421
import java.util.stream.StreamSupport;
2522

@@ -57,6 +54,7 @@ public class CodeAnalyzerIntegrationTest {
5754
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-corrupt-test")), "/test-applications/mvnw-corrupt-test")
5855
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/plantsbywebsphere")), "/test-applications/plantsbywebsphere")
5956
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/call-graph-test")), "/test-applications/call-graph-test")
57+
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/record-class-test")), "/test-applications/record-class-test")
6058
.withCopyFileToContainer(MountableFile.forHostPath(Paths.get(System.getProperty("user.dir")).resolve("src/test/resources/test-applications/mvnw-working-test")), "/test-applications/mvnw-working-test");
6159

6260
@Container
@@ -254,4 +252,51 @@ void shouldBeAbleToDetectCRUDOperationsAndQueriesForPlantByWebsphere() throws Ex
254252
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudOperation), "Expected CRUD operation JSON structure not found");
255253
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudQuery), "Expected CRUD query JSON structure not found");
256254
}
257-
}
255+
256+
@Test
257+
void symbolTableShouldHaveRecords() throws IOException, InterruptedException {
258+
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
259+
"bash", "-c",
260+
String.format(
261+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
262+
javaHomePath, codeanalyzerVersion
263+
)
264+
);
265+
266+
// Read the output JSON
267+
Gson gson = new Gson();
268+
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
269+
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
270+
Assertions.assertEquals(4, symbolTable.size(), "Symbol table should have 4 records");
271+
}
272+
273+
@Test
274+
void symbolTableShouldHaveDefaultRecordComponents() throws IOException, InterruptedException {
275+
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
276+
"bash", "-c",
277+
String.format(
278+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
279+
javaHomePath, codeanalyzerVersion
280+
)
281+
);
282+
283+
// Read the output JSON
284+
Gson gson = new Gson();
285+
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
286+
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
287+
for (Map.Entry<String, JsonElement> element : symbolTable.entrySet()) {
288+
String key = element.getKey();
289+
if (!key.endsWith("PersonRecord.java")) {
290+
continue;
291+
}
292+
JsonObject type = element.getValue().getAsJsonObject();
293+
if (type.has("type_declarations")) {
294+
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
295+
JsonArray recordComponent = typeDeclarations.getAsJsonObject("org.example.PersonRecord").getAsJsonArray("record_components");
296+
Assertions.assertEquals(2, recordComponent.size(), "Record component should have 2 components");
297+
JsonObject record = recordComponent.get(1).getAsJsonObject();
298+
Assertions.assertTrue(record.get("name").getAsString().equals("age") && record.get("default_value").getAsInt() == 18, "Record component should have a name");
299+
}
300+
}
301+
}
302+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
10+
# Binary files should be left untouched
11+
*.jar binary
12+
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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* This file was generated by the Gradle 'init' task.
3+
*
4+
* This generated file contains a sample Java application project to get you started.
5+
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.12.1/userguide/building_java_projects.html in the Gradle documentation.
6+
* This project uses @Incubating APIs which are subject to change.
7+
*/
8+
9+
plugins {
10+
// Apply the application plugin to add support for building a CLI application in Java.
11+
application
12+
}
13+
14+
repositories {
15+
// Use Maven Central for resolving dependencies.
16+
mavenCentral()
17+
}
18+
19+
dependencies {
20+
// This dependency is used by the application.
21+
implementation(libs.guava)
22+
}
23+
24+
testing {
25+
suites {
26+
// Configure the built-in test suite
27+
val test by getting(JvmTestSuite::class) {
28+
// Use JUnit Jupiter test framework
29+
useJUnitJupiter("5.11.1")
30+
}
31+
}
32+
}
33+
34+
// Apply a specific Java toolchain to ease working on different environments.
35+
java {
36+
toolchain {
37+
languageVersion = JavaLanguageVersion.of(17)
38+
}
39+
}
40+
41+
application {
42+
// Define the main class for the application.
43+
mainClass = "org.example.App"
44+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* This source file was generated by the Gradle 'init' task
3+
*/
4+
package org.example;
5+
6+
public class App {
7+
public static void main(String[] args) {
8+
// Create instances of records
9+
PersonRecord person = new PersonRecord("Alice", 30);
10+
CarRecord car = new CarRecord("Tesla Model 3", 2023);
11+
// Access public fields and methods
12+
System.out.println(person.greet());
13+
System.out.println(car.getCarDetails());
14+
15+
// Access package-private method (allowed within the same package)
16+
System.out.println("Person Internal Info: " + person.internalInfo());
17+
System.out.println("Car Internal VIN: " + car.getInternalVIN());
18+
}
19+
}

0 commit comments

Comments
 (0)