Skip to content

Address Issue 118: Support Record Declarations #121

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 2 commits into from
Feb 18, 2025
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
10 changes: 7 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ plugins {
id 'eclipse'
id 'application'
id 'org.graalvm.buildtools.native' version '0.10.4'
id 'org.jetbrains.kotlin.jvm'
}

// Get the version from the property file first
Expand All @@ -30,8 +31,6 @@ repositories {
}

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

if (project.hasProperty('mainClass')) {
Expand Down Expand Up @@ -121,7 +120,8 @@ dependencies {
implementation('org.jgrapht:jgrapht-core:1.5.2')
implementation('org.jgrapht:jgrapht-io:1.5.2')
implementation('org.jgrapht:jgrapht-ext:1.5.2')
implementation('com.github.javaparser:javaparser-symbol-solver-core:3.25.9')
implementation('com.github.javaparser:javaparser-symbol-solver-core:3.26.3')
implementation('com.github.javaparser:javaparser-core:3.26.3')

// TestContainers
testImplementation 'org.testcontainers:testcontainers:1.19.3'
Expand All @@ -135,6 +135,7 @@ dependencies {
// SLF4J - for TestContainers logging
testImplementation 'org.slf4j:slf4j-api:2.0.9'
testImplementation 'org.slf4j:slf4j-simple:2.0.9'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

}

Expand Down Expand Up @@ -277,3 +278,6 @@ tasks.register('bumpVersion') {
}

nativeCompile.finalizedBy copyNativeExecutable
kotlin {
jvmToolchain(11)
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=2.2.1
version=2.3.0
9 changes: 8 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
/*
pluginManagement {
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.1.10'
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}/*
Copyright IBM Corporation 2023, 2024

Licensed under the Apache Public License 2.0, Version 2.0 (the "License");
Expand Down
67 changes: 60 additions & 7 deletions src/main/java/com/ibm/cldk/SymbolTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.ibm.cldk.entities.*;
import com.ibm.cldk.utils.Log;
import org.apache.commons.lang3.tuple.Pair;
import org.checkerframework.checker.units.qual.C;

import java.io.IOException;
import java.nio.file.Path;
Expand Down Expand Up @@ -110,8 +111,7 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
cUnit.setTypeDeclarations(parseResult.findAll(TypeDeclaration.class).stream().filter(typeDecl -> typeDecl.getFullyQualifiedName().isPresent()).map(typeDecl -> {
// get type name and initialize the type object
String typeName = typeDecl.getFullyQualifiedName().get().toString();
com.ibm.cldk.entities.Type typeNode = new com.ibm.cldk.entities.Type();

com.ibm.cldk.entities.Type typeNode = new com.ibm.cldk.entities.Type();;
if (typeDecl instanceof ClassOrInterfaceDeclaration) {
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) typeDecl;

Expand Down Expand Up @@ -146,8 +146,26 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR

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

} else {
// Set that this is a record declaration
typeNode.setRecordDeclaration(typeDecl.isRecordDeclaration());

// Add interfaces implemented by record
typeNode.setImplementsList(recordDecl.getImplementedTypes().stream().map(SymbolTable::resolveType).collect(Collectors.toList()));

// Add record modifiers
typeNode.setModifiers(recordDecl.getModifiers().stream().map(m -> m.toString().strip()).collect(Collectors.toList()));

// Add record annotations
typeNode.setAnnotations(recordDecl.getAnnotations().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));

// Add record components
typeNode.setRecordComponents(processRecordComponents(recordDecl));
}
else {
// TODO: handle AnnotationDeclaration, RecordDeclaration
// set the common type attributes only
Log.warn("Found unsupported type declaration: " + typeDecl.toString());
Expand All @@ -162,7 +180,6 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
typeNode.setClassOrInterfaceDeclaration(typeDecl.isClassOrInterfaceDeclaration());
typeNode.setEnumDeclaration(typeDecl.isEnumDeclaration());
typeNode.setAnnotationDeclaration(typeDecl.isAnnotationDeclaration());
typeNode.setRecordDeclaration(typeDecl.isRecordDeclaration());

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


private static List<RecordComponent> processRecordComponents(RecordDeclaration recordDecl) {
return recordDecl.getParameters().stream().map(
parameter -> {
RecordComponent recordComponent = new RecordComponent();
recordComponent.setName(parameter.getNameAsString());
recordComponent.setType(resolveType(parameter.getType()));
recordComponent.setAnnotations(parameter.getAnnotations().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
recordComponent.setModifiers(parameter.getModifiers().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
recordComponent.setVarArgs(parameter.isVarArgs());
recordComponent.setDefaultValue(mapRecordConstructorDefaults(recordDecl).getOrDefault(parameter.getNameAsString(), null));
return recordComponent;
}
).collect(Collectors.toList());
}

private static Map<String, Object> mapRecordConstructorDefaults(RecordDeclaration recordDecl) {

return recordDecl.getCompactConstructors().stream()
.flatMap(constructor -> constructor.findAll(AssignExpr.class).stream()) // Flatten all assignments
.filter(assignExpr -> assignExpr.getTarget().isNameExpr()) // Ensure assignment is to a parameter
.collect(Collectors.toMap(
assignExpr -> assignExpr.getTarget().asNameExpr().getNameAsString(), // Key: Parameter Name
assignExpr -> Optional.ofNullable(assignExpr.getValue()).map(valueExpr -> { // Value: Default Value
return valueExpr.isStringLiteralExpr() ? valueExpr.asStringLiteralExpr().asString()
: valueExpr.isBooleanLiteralExpr() ? valueExpr.asBooleanLiteralExpr().getValue()
: valueExpr.isCharLiteralExpr() ? valueExpr.asCharLiteralExpr().getValue()
: valueExpr.isDoubleLiteralExpr() ? valueExpr.asDoubleLiteralExpr().asDouble()
: valueExpr.isIntegerLiteralExpr() ? valueExpr.asIntegerLiteralExpr().asNumber()
: valueExpr.isLongLiteralExpr() ? valueExpr.asLongLiteralExpr().asNumber()
: valueExpr.isNullLiteralExpr() ? null
: valueExpr.toString();}).orElse("null"))); // Default: store as a string
}

private static boolean isEntryPointClass(TypeDeclaration typeDecl) {
return isSpringEntrypointClass(typeDecl) || isStrutsEntryPointClass(typeDecl) || isCamelEntryPointClass(typeDecl) || isJaxRSEntrypointClass(typeDecl) || isJakartaServletEntryPointClass(typeDecl);

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

ParserConfiguration parserConfiguration = new ParserConfiguration();
ParserConfiguration parserConfiguration = new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21);
parserConfiguration.setSymbolResolver(new JavaSymbolSolver(combinedTypeSolver));

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

// create java parser with the configuration
Expand All @@ -972,6 +1024,7 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
ParseResult<CompilationUnit> parseResult = javaParser.parse(javaFilePath);
if (parseResult.isSuccessful()) {
CompilationUnit compilationUnit = parseResult.getResult().get();
System.out.println("Successfully parsed file: " + javaFilePath.toString());
symbolTable.put(compilationUnit.getStorage().get().getPath().toString(), processCompilationUnit(compilationUnit));
} else {
Log.error(parseResult.getProblems().toString());
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/ibm/cldk/entities/RecordComponent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ibm.cldk.entities;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
public class RecordComponent {
private String comment;
private String name;
private String type;
private List<String> modifiers;
private List<String> annotations = new ArrayList<>();
private Object defaultValue = null; // We will store the string representation of the default value
private boolean isVarArgs = false;
}
1 change: 1 addition & 0 deletions src/main/java/com/ibm/cldk/entities/Type.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ public class Type {
private Map<String, Callable> callableDeclarations = new HashMap<>();
private List<Field> fieldDeclarations = new ArrayList<>();
private List<EnumConstant> enumConstants = new ArrayList<>();
private List<RecordComponent> recordComponents = new ArrayList<>();
private boolean isEntrypointClass = false;
}
55 changes: 50 additions & 5 deletions src/test/java/com/ibm/cldk/CodeAnalyzerIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
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 @@ -19,7 +16,7 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Map;
import java.util.Properties;
import java.util.stream.StreamSupport;

Expand Down Expand Up @@ -57,6 +54,7 @@ public class CodeAnalyzerIntegrationTest {
.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/record-class-test")), "/test-applications/record-class-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 Down Expand Up @@ -254,4 +252,51 @@ void shouldBeAbleToDetectCRUDOperationsAndQueriesForPlantByWebsphere() throws Ex
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudOperation), "Expected CRUD operation JSON structure not found");
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudQuery), "Expected CRUD query JSON structure not found");
}
}

@Test
void symbolTableShouldHaveRecords() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);

// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
Assertions.assertEquals(4, symbolTable.size(), "Symbol table should have 4 records");
}

@Test
void symbolTableShouldHaveDefaultRecordComponents() throws IOException, InterruptedException {
var runCodeAnalyzerOnCallGraphTest = container.execInContainer(
"bash", "-c",
String.format(
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/record-class-test --analysis-level=1",
javaHomePath, codeanalyzerVersion
)
);

// Read the output JSON
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(runCodeAnalyzerOnCallGraphTest.getStdout(), JsonObject.class);
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
for (Map.Entry<String, JsonElement> element : symbolTable.entrySet()) {
String key = element.getKey();
if (!key.endsWith("PersonRecord.java")) {
continue;
}
JsonObject type = element.getValue().getAsJsonObject();
if (type.has("type_declarations")) {
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
JsonArray recordComponent = typeDeclarations.getAsJsonObject("org.example.PersonRecord").getAsJsonArray("record_components");
Assertions.assertEquals(2, recordComponent.size(), "Record component should have 2 components");
JsonObject record = recordComponent.get(1).getAsJsonObject();
Assertions.assertTrue(record.get("name").getAsString().equals("age") && record.get("default_value").getAsInt() == 18, "Record component should have a name");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#
# 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

# Binary files should be left untouched
*.jar binary

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* 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.
* This project uses @Incubating APIs which are subject to change.
*/

plugins {
// Apply the application plugin to add support for building a CLI application in Java.
application
}

repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}

dependencies {
// This dependency is used by the application.
implementation(libs.guava)
}

testing {
suites {
// Configure the built-in test suite
val test by getting(JvmTestSuite::class) {
// Use JUnit Jupiter test framework
useJUnitJupiter("5.11.1")
}
}
}

// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

application {
// Define the main class for the application.
mainClass = "org.example.App"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;

public class App {
public static void main(String[] args) {
// Create instances of records
PersonRecord person = new PersonRecord("Alice", 30);
CarRecord car = new CarRecord("Tesla Model 3", 2023);
// Access public fields and methods
System.out.println(person.greet());
System.out.println(car.getCarDetails());

// Access package-private method (allowed within the same package)
System.out.println("Person Internal Info: " + person.internalInfo());
System.out.println("Car Internal VIN: " + car.getInternalVIN());
}
}
Loading