Skip to content

Feature Request Issue 101: Add a support to be able to Capture static and instance initialization blocks in java classes #123

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
4 changes: 4 additions & 0 deletions .github/workflows/release_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
{
"title": "## \uD83D\uDEE0 Other Updates",
"labels": ["other", "kind/dependency-change"]
},
{
"title": "## 🚨 Breaking Changes",
"labels": ["breaking"]
}
],
"ignore_labels": [
Expand Down
55 changes: 50 additions & 5 deletions src/main/java/com/ibm/cldk/SymbolTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
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 @@ -172,9 +171,16 @@ else if (typeDecl instanceof RecordDeclaration) {
typeNode = new com.ibm.cldk.entities.Type();
}

/* set common attributes of types that available in type declarations:
is nested type, is class or interface declaration, is enum declaration,
comments, parent class, callable declarations, field declarations */
/* set common attributes of types that available in type declarations:
is nested type, is class or interface declaration, is enum declaration,
comments, parent class, callable declarations, field declarations
*/
// Discover initialization blocks
typeNode.setInitializationBlocks(typeDecl.findAll(InitializerDeclaration.class).stream()
.map(initializerDeclaration -> {
return createInitializationBlock(initializerDeclaration, parseResult.getStorage().map(s -> s.getPath().toString()).orElse("<in-memory>"));
})
.collect(Collectors.toList()));
// Set fields indicating nested, class/interface, enum, annotation, and record types
typeNode.setNestedType(typeDecl.isNestedType());
typeNode.setClassOrInterfaceDeclaration(typeDecl.isClassOrInterfaceDeclaration());
Expand Down Expand Up @@ -213,7 +219,38 @@ else if (typeDecl instanceof RecordDeclaration) {
return cUnit;
}


private static InitializationBlock createInitializationBlock(InitializerDeclaration initializerDeclaration, String filePath) {
InitializationBlock initializationBlock = new InitializationBlock();
initializationBlock.setFilePath(filePath);
initializationBlock.setComment(initializerDeclaration.getComment().isPresent() ? initializerDeclaration.getComment().get().asString() : "");
initializationBlock.setAnnotations(initializerDeclaration.getAnnotations().stream().map(a -> a.toString().strip()).collect(Collectors.toList()));
// add exceptions declared in "throws" clause
initializationBlock.setThrownExceptions(initializerDeclaration.getBody().getStatements().stream().filter(Statement::isThrowStmt).map(throwStmt -> {
try {
return javaSymbolSolver.calculateType(throwStmt.asThrowStmt().getExpression()).describe();
} catch (Exception e) {
return throwStmt.asThrowStmt().getExpression().toString();
}
}).collect(Collectors.toList()));
initializationBlock.setCode(initializerDeclaration.getBody().toString());
initializationBlock.setStartLine(initializerDeclaration.getRange().isPresent() ? initializerDeclaration.getRange().get().begin.line : -1);
initializationBlock.setEndLine(initializerDeclaration.getRange().isPresent() ? initializerDeclaration.getRange().get().end.line : -1);
initializationBlock.setStatic(initializerDeclaration.isStatic());
initializationBlock.setReferencedTypes(getReferencedTypes(Optional.ofNullable(initializerDeclaration.getBody())));
initializationBlock.setAccessedFields(getAccessedFields(Optional.ofNullable(initializerDeclaration.getBody()), Collections.emptyList(), ""));
initializationBlock.setCallSites(getCallSites(Optional.ofNullable(initializerDeclaration.getBody())));
initializationBlock.setVariableDeclarations(getVariableDeclarations(Optional.ofNullable(initializerDeclaration.getBody())));
initializationBlock.setCyclomaticComplexity(getCyclomaticComplexity(initializerDeclaration));
return initializationBlock;
}
/**
* Processes the given record to extract information about the
* declared field and returns a JSON object containing the extracted
* information.
*
* @param recordDecl field declaration to be processed
* @return Field object containing extracted information
*/
private static List<RecordComponent> processRecordComponents(RecordDeclaration recordDecl) {
return recordDecl.getParameters().stream().map(
parameter -> {
Expand Down Expand Up @@ -568,6 +605,14 @@ private static int getCyclomaticComplexity(CallableDeclaration callableDeclarati
int catchClauseCount = callableDeclaration.findAll(CatchClause.class).size();
return ifStmtCount + loopStmtCount + switchCaseCount + conditionalExprCount + catchClauseCount + 1;
}
private static int getCyclomaticComplexity(InitializerDeclaration initializerDeclaration) {
int ifStmtCount = initializerDeclaration.findAll(IfStmt.class).size();
int loopStmtCount = initializerDeclaration.findAll(DoStmt.class).size() + initializerDeclaration.findAll(ForStmt.class).size() + initializerDeclaration.findAll(ForEachStmt.class).size() + initializerDeclaration.findAll(WhileStmt.class).size();
int switchCaseCount = initializerDeclaration.findAll(SwitchStmt.class).stream().map(stmt -> stmt.getEntries().size()).reduce(0, Integer::sum);
int conditionalExprCount = initializerDeclaration.findAll(ConditionalExpr.class).size();
int catchClauseCount = initializerDeclaration.findAll(CatchClause.class).size();
return ifStmtCount + loopStmtCount + switchCaseCount + conditionalExprCount + catchClauseCount + 1;
}

/**
* Processes the given field declaration to extract information about the
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/ibm/cldk/entities/InitializationBlock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ibm.cldk.entities;

import lombok.Data;

import java.util.List;
import java.util.stream.Collector;

@Data
public class InitializationBlock {
private String filePath;
private String comment;
private List<String> annotations;
private List<String> thrownExceptions;
private String code;
private int startLine;
private int endLine;
private boolean isStatic;
private List<String> referencedTypes;
private List<String> accessedFields;
private List<CallSite> callSites;
private List<VariableDeclaration> variableDeclarations;
private int cyclomaticComplexity;

}
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 @@ -28,5 +28,6 @@ public class Type {
private List<Field> fieldDeclarations = new ArrayList<>();
private List<EnumConstant> enumConstants = new ArrayList<>();
private List<RecordComponent> recordComponents = new ArrayList<>();
private List<InitializationBlock> initializationBlocks = new ArrayList<>();
private boolean isEntrypointClass = false;
}
32 changes: 32 additions & 0 deletions src/test/java/com/ibm/cldk/CodeAnalyzerIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class CodeAnalyzerIntegrationTest {
.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/init-blocks-test")), "/test-applications/init-blocks-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 @@ -332,4 +333,35 @@ void parametersInCallableMustHaveStartAndEndLineAndColumns() throws IOException,
}
}
}

@Test
void mustBeAbleToResolveInitializationBlocks() 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/init-blocks-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");
JsonArray initializationBlocks = typeDeclarations.getAsJsonObject("org.example.App").getAsJsonArray("initialization_blocks");
// There should be 2 blocks
Assertions.assertEquals(2, initializationBlocks.size(), "Callable should have 1 parameter");
Assertions.assertTrue(initializationBlocks.get(0).getAsJsonObject().get("is_static").getAsBoolean(), "Static block should be marked as static");
Assertions.assertFalse(initializationBlocks.get(1).getAsJsonObject().get("is_static").getAsBoolean(), "Instance block should be marked as not static");
}
}
}
}
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,43 @@
package org.example;

import java.util.List;

public class App {
private static String staticMessage;

static {
try {
staticMessage = "Static block initialized";
System.out.println("Static initialization block executed.");
initializeStaticFields();
} catch (Exception e) {
System.err.println("Error in static block: " + e.getMessage());
throw new RuntimeException(e);
}
}

{
try {
System.out.println("Instance initialization block executed.");
initializeInstanceFields();
} catch (Exception e) {
System.err.println("Error in instance block: " + e.getMessage());
}
}

public App() {
System.out.println("Constructor executed.");
}

private static void initializeStaticFields() {
System.out.println("Initializing static fields.");
}

private void initializeInstanceFields() {
System.out.println("Initializing instance fields.");
}

public static void main(String[] args) {
new App();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties

org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true

Loading