Skip to content

Address Issue 103: Extend ParameterInCallable class to capture line and column offsets #122

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
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
71 changes: 64 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 @@ -350,6 +401,10 @@ private static ParameterInCallable processParameterDeclaration(Parameter paramDe
parameter.setName(paramDecl.getName().toString());
parameter.setAnnotations(paramDecl.getAnnotations().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
parameter.setModifiers(paramDecl.getModifiers().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
parameter.setStartLine(paramDecl.getRange().isPresent() ? paramDecl.getRange().get().begin.line : -1);
parameter.setStartColumn(paramDecl.getRange().isPresent() ? paramDecl.getRange().get().begin.column : -1);
parameter.setEndLine(paramDecl.getRange().isPresent() ? paramDecl.getRange().get().end.line : -1);
parameter.setEndColumn(paramDecl.getRange().isPresent() ? paramDecl.getRange().get().end.column : -1);
return parameter;
}

Expand Down Expand Up @@ -904,7 +959,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 +982,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 +1013,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 +1028,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
4 changes: 4 additions & 0 deletions src/main/java/com/ibm/cldk/entities/ParameterInCallable.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ public class ParameterInCallable {
private String name;
private List<String> annotations;
private List<String> modifiers;
private int startLine;
private int endLine;
private int startColumn;
private int endColumn;
}
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;
}
88 changes: 83 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,84 @@ 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");
}
}
}

@Test
void parametersInCallableMustHaveStartAndEndLineAndColumns() 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("App.java")) {
continue;
}
JsonObject type = element.getValue().getAsJsonObject();
if (type.has("type_declarations")) {
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
JsonObject mainMethod = typeDeclarations.getAsJsonObject("org.example.App").getAsJsonObject("callable_declarations").getAsJsonObject("main(String[])");
JsonArray parameters = mainMethod.getAsJsonArray("parameters");
// There should be 1 parameter
Assertions.assertEquals(1, parameters.size(), "Callable should have 1 parameter");
JsonObject parameter = parameters.get(0).getAsJsonObject();
// Start and end line and column should not be -1
Assertions.assertTrue(parameter.get("start_line").getAsInt() == 7 && parameter.get("end_line").getAsInt() == 7 && parameter.get("start_column").getAsInt() == 29 && parameter.get("end_column").getAsInt() == 41, "Parameter should have start and end line and columns");
}
}
}
}
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
Loading