Skip to content

Commit 7d09491

Browse files
authored
Merge pull request #144 from tylerstennett/feat/java-import-model
Add new import schema with path, is_static, and is_wildcard fields
2 parents 3676cd8 + ee3a7fd commit 7d09491

8 files changed

Lines changed: 292 additions & 37 deletions

File tree

gradle.properties

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

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,38 @@ private static void emit(String consolidatedJSONString) throws IOException {
251251
}
252252
}
253253

254+
private static boolean hasLegacyImportSchema(JsonObject symbolTableJson) {
255+
if (symbolTableJson == null) {
256+
return false;
257+
}
258+
for (Map.Entry<String, JsonElement> entry : symbolTableJson.entrySet()) {
259+
JsonElement compilationUnitElement = entry.getValue();
260+
if (!compilationUnitElement.isJsonObject()) {
261+
continue;
262+
}
263+
JsonObject compilationUnitJson = compilationUnitElement.getAsJsonObject();
264+
if (!compilationUnitJson.has("imports") || !compilationUnitJson.get("imports").isJsonArray()) {
265+
continue;
266+
}
267+
for (JsonElement importElement : compilationUnitJson.getAsJsonArray("imports")) {
268+
if (importElement.isJsonPrimitive() && importElement.getAsJsonPrimitive().isString()) {
269+
return true;
270+
}
271+
}
272+
}
273+
return false;
274+
}
275+
254276
private static Map<String, JavaCompilationUnit> readSymbolTableFromFile(File analysisJsonFile) {
255277
Type symbolTableType = new TypeToken<Map<String, JavaCompilationUnit>>() {
256278
}.getType();
257279
try (FileReader reader = new FileReader(analysisJsonFile)) {
258280
JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject();
259-
return gson.fromJson(jsonObject.get("symbol_table"), symbolTableType);
281+
JsonObject symbolTableJson = jsonObject.getAsJsonObject("symbol_table");
282+
if (hasLegacyImportSchema(symbolTableJson)) {
283+
throw new IllegalStateException("Existing analysis.json uses legacy import schema (imports as strings). Regenerate analysis with codeanalyzer 2.3.7 or newer.");
284+
}
285+
return gson.fromJson(symbolTableJson, symbolTableType);
260286
} catch (IOException e) {
261287
Log.error("Error reading analysis file: " + e.getMessage());
262288
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,13 @@ private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseR
133133
// Add javadoc comment
134134
// Add imports
135135
cUnit.setImports(
136-
parseResult.getImports().stream().map(NodeWithName::getNameAsString).collect(Collectors.toList()));
136+
parseResult.getImports().stream().map(importDecl -> {
137+
Import importNode = new Import();
138+
importNode.setPath(importDecl.getNameAsString());
139+
importNode.setStatic(importDecl.isStatic());
140+
importNode.setWildcard(importDecl.isAsterisk());
141+
return importNode;
142+
}).collect(Collectors.toList()));
137143

138144
// create array node for type declarations
139145
cUnit.setTypeDeclarations(parseResult.findAll(TypeDeclaration.class).stream()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.ibm.cldk.entities;
2+
3+
import lombok.Data;
4+
5+
/** Represents an import declaration in a Java compilation unit. */
6+
@Data
7+
public class Import {
8+
private String path;
9+
private boolean isStatic = false;
10+
private boolean isWildcard = false;
11+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class JavaCompilationUnit {
1010
private String filePath;
1111
private String packageName;
1212
private List<Comment> comments = new ArrayList<>();
13-
private List<String> imports;
13+
private List<Import> imports;
1414
private Map<String, Type> typeDeclarations;
1515
private boolean isModified;
1616
}

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

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -214,45 +214,92 @@ void shouldBeAbleToDetectCRUDOperationsAndQueriesForPlantByWebsphere() throws Ex
214214
var runCodeAnalyzerOnPlantsByWebsphere = container.execInContainer(
215215
"bash", "-c",
216216
String.format(
217-
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/plantsbywebsphere --analysis-level=1 --verbose",
217+
"export JAVA_HOME=%s && java -jar /opt/jars/codeanalyzer-%s.jar --input=/test-applications/plantsbywebsphere --analysis-level=1",
218218
javaHomePath, codeanalyzerVersion
219219
)
220220
);
221221

222222

223+
Assertions.assertEquals(0, runCodeAnalyzerOnPlantsByWebsphere.getExitCode(), "CodeAnalyzer command should succeed");
223224
String output = runCodeAnalyzerOnPlantsByWebsphere.getStdout();
225+
Gson gson = new Gson();
226+
JsonObject jsonObject = gson.fromJson(output, JsonObject.class);
227+
JsonObject symbolTable = jsonObject.getAsJsonObject("symbol_table");
228+
Assertions.assertNotNull(symbolTable);
229+
Assertions.assertTrue(symbolTable.size() > 0, "Symbol table should not be empty");
230+
231+
boolean hasReadOperation = false;
232+
boolean hasCreateOperation = false;
233+
boolean hasUpdateOperation = false;
234+
boolean hasNamedQuery = false;
235+
int crudOperationCount = 0;
236+
int crudQueryCount = 0;
237+
238+
for (Map.Entry<String, JsonElement> compilationUnitEntry : symbolTable.entrySet()) {
239+
JsonObject compilationUnit = compilationUnitEntry.getValue().getAsJsonObject();
240+
if (!compilationUnit.has("type_declarations")) {
241+
continue;
242+
}
243+
JsonObject typeDeclarations = compilationUnit.getAsJsonObject("type_declarations");
244+
for (Map.Entry<String, JsonElement> typeEntry : typeDeclarations.entrySet()) {
245+
JsonObject typeDeclaration = typeEntry.getValue().getAsJsonObject();
246+
if (!typeDeclaration.has("callable_declarations")) {
247+
continue;
248+
}
249+
JsonObject callableDeclarations = typeDeclaration.getAsJsonObject("callable_declarations");
250+
for (Map.Entry<String, JsonElement> callableEntry : callableDeclarations.entrySet()) {
251+
JsonObject callable = callableEntry.getValue().getAsJsonObject();
252+
JsonArray crudOperations = callable.getAsJsonArray("crud_operations");
253+
if (crudOperations != null) {
254+
for (JsonElement crudOperationElement : crudOperations) {
255+
JsonObject crudOperation = crudOperationElement.getAsJsonObject();
256+
crudOperationCount++;
257+
Assertions.assertTrue(crudOperation.has("line_number"), "CRUD operation should have line_number");
258+
Assertions.assertTrue(crudOperation.has("operation_type"), "CRUD operation should have operation_type");
259+
Assertions.assertTrue(crudOperation.has("target_table"), "CRUD operation should have target_table");
260+
Assertions.assertTrue(crudOperation.has("involved_columns"), "CRUD operation should have involved_columns");
261+
Assertions.assertTrue(crudOperation.has("condition"), "CRUD operation should have condition");
262+
Assertions.assertTrue(crudOperation.has("joined_tables"), "CRUD operation should have joined_tables");
263+
String operationType = crudOperation.get("operation_type").getAsString();
264+
int lineNumber = crudOperation.get("line_number").getAsInt();
265+
Assertions.assertTrue(lineNumber > 0, "CRUD operation should have positive line_number");
266+
if ("READ".equals(operationType)) {
267+
hasReadOperation = true;
268+
}
269+
if ("CREATE".equals(operationType)) {
270+
hasCreateOperation = true;
271+
}
272+
if ("UPDATE".equals(operationType)) {
273+
hasUpdateOperation = true;
274+
}
275+
}
276+
}
277+
JsonArray crudQueries = callable.getAsJsonArray("crud_queries");
278+
if (crudQueries != null) {
279+
for (JsonElement crudQueryElement : crudQueries) {
280+
JsonObject crudQuery = crudQueryElement.getAsJsonObject();
281+
crudQueryCount++;
282+
Assertions.assertTrue(crudQuery.has("line_number"), "CRUD query should have line_number");
283+
Assertions.assertTrue(crudQuery.has("query_type"), "CRUD query should have query_type");
284+
Assertions.assertTrue(crudQuery.has("query_arguments"), "CRUD query should have query_arguments");
285+
String queryType = crudQuery.get("query_type").getAsString();
286+
int lineNumber = crudQuery.get("line_number").getAsInt();
287+
Assertions.assertTrue(lineNumber > 0, "CRUD query should have positive line_number");
288+
if ("NAMED".equals(queryType)) {
289+
hasNamedQuery = true;
290+
}
291+
}
292+
}
293+
}
294+
}
295+
}
224296

225-
Assertions.assertTrue(output.contains("\"query_type\": \"NAMED\""), "No entry point classes found");
226-
Assertions.assertTrue(output.contains("\"operation_type\": \"READ\""), "No entry point methods found");
227-
Assertions.assertTrue(output.contains("\"operation_type\": \"UPDATE\""), "No entry point methods found");
228-
Assertions.assertTrue(output.contains("\"operation_type\": \"CREATE\""), "No entry point methods found");
229-
230-
// Convert the expected JSON structure into a string
231-
String expectedCrudOperation =
232-
"\"crud_operations\": [" +
233-
"{" +
234-
"\"line_number\": 115," +
235-
"\"operation_type\": \"READ\"," +
236-
"\"target_table\": null," +
237-
"\"involved_columns\": null," +
238-
"\"condition\": null," +
239-
"\"joined_tables\": null" +
240-
"}]";
241-
242-
// Expected JSON for CRUD Queries
243-
String expectedCrudQuery =
244-
"\"crud_queries\": [" +
245-
"{" +
246-
"\"line_number\": 141,";
247-
248-
// Normalize the output and expected strings to ignore formatting differences
249-
String normalizedOutput = output.replaceAll("\\s+", "");
250-
String normalizedExpectedCrudOperation = expectedCrudOperation.replaceAll("\\s+", "");
251-
String normalizedExpectedCrudQuery = expectedCrudQuery.replaceAll("\\s+", "");
252-
253-
// Assertions for both CRUD operations and queries
254-
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudOperation), "Expected CRUD operation JSON structure not found");
255-
Assertions.assertTrue(normalizedOutput.contains(normalizedExpectedCrudQuery), "Expected CRUD query JSON structure not found");
297+
Assertions.assertTrue(crudOperationCount > 0, "No CRUD operations found");
298+
Assertions.assertTrue(crudQueryCount > 0, "No CRUD queries found");
299+
Assertions.assertTrue(hasNamedQuery, "No NAMED CRUD query found");
300+
Assertions.assertTrue(hasReadOperation, "No READ CRUD operation found");
301+
Assertions.assertTrue(hasCreateOperation, "No CREATE CRUD operation found");
302+
Assertions.assertTrue(hasUpdateOperation, "No UPDATE CRUD operation found");
256303
}
257304

258305
@Test
@@ -324,7 +371,9 @@ void parametersInCallableMustHaveStartAndEndLineAndColumns() throws IOException,
324371
JsonObject type = element.getValue().getAsJsonObject();
325372
if (type.has("type_declarations")) {
326373
JsonObject typeDeclarations = type.getAsJsonObject("type_declarations");
327-
JsonObject mainMethod = typeDeclarations.getAsJsonObject("org.example.App").getAsJsonObject("callable_declarations").getAsJsonObject("main(String[])");
374+
JsonObject mainMethod = typeDeclarations.getAsJsonObject("org.example.App")
375+
.getAsJsonObject("callable_declarations")
376+
.getAsJsonObject("main(java.lang.String[])");
328377
JsonArray parameters = mainMethod.getAsJsonArray("parameters");
329378
// There should be 1 parameter
330379
Assertions.assertEquals(1, parameters.size(), "Callable should have 1 parameter");
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.ibm.cldk;
2+
3+
import com.ibm.cldk.entities.Import;
4+
import com.ibm.cldk.entities.JavaCompilationUnit;
5+
import java.io.File;
6+
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.List;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.Assertions;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.io.TempDir;
16+
17+
public class CodeAnalyzerTest {
18+
19+
@TempDir
20+
Path tempDir;
21+
22+
@SuppressWarnings("unchecked")
23+
private Map<String, JavaCompilationUnit> invokeReadSymbolTableFromFile(Path analysisFilePath) throws Exception {
24+
Method readSymbolTableMethod = CodeAnalyzer.class.getDeclaredMethod("readSymbolTableFromFile", File.class);
25+
readSymbolTableMethod.setAccessible(true);
26+
try {
27+
return (Map<String, JavaCompilationUnit>) readSymbolTableMethod.invoke(null, analysisFilePath.toFile());
28+
} catch (InvocationTargetException invocationTargetException) {
29+
Throwable targetException = invocationTargetException.getTargetException();
30+
if (targetException instanceof Exception) {
31+
throw (Exception) targetException;
32+
}
33+
throw new RuntimeException(targetException);
34+
}
35+
}
36+
37+
private Path writeAnalysisFile(String jsonContent) throws Exception {
38+
Path analysisFilePath = tempDir.resolve("analysis.json");
39+
Files.writeString(analysisFilePath, jsonContent, StandardCharsets.UTF_8);
40+
return analysisFilePath;
41+
}
42+
43+
@Test
44+
public void testReadSymbolTableFromFileRejectsLegacyImportSchema() throws Exception {
45+
String jsonContent = "{\n"
46+
+ " \"symbol_table\": {\n"
47+
+ " \"/tmp/T.java\": {\n"
48+
+ " \"file_path\": \"/tmp/T.java\",\n"
49+
+ " \"package_name\": \"\",\n"
50+
+ " \"comments\": [],\n"
51+
+ " \"imports\": [\"java.util.List\"],\n"
52+
+ " \"type_declarations\": {},\n"
53+
+ " \"is_modified\": false\n"
54+
+ " }\n"
55+
+ " }\n"
56+
+ "}\n";
57+
Path analysisFilePath = writeAnalysisFile(jsonContent);
58+
59+
IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class,
60+
() -> invokeReadSymbolTableFromFile(analysisFilePath));
61+
Assertions.assertTrue(exception.getMessage().contains("legacy import schema"));
62+
}
63+
64+
@Test
65+
public void testReadSymbolTableFromFileParsesExplicitImportSchema() throws Exception {
66+
String jsonContent = "{\n"
67+
+ " \"symbol_table\": {\n"
68+
+ " \"/tmp/T.java\": {\n"
69+
+ " \"file_path\": \"/tmp/T.java\",\n"
70+
+ " \"package_name\": \"\",\n"
71+
+ " \"comments\": [],\n"
72+
+ " \"imports\": [\n"
73+
+ " {\n"
74+
+ " \"path\": \"java.util.List\",\n"
75+
+ " \"is_static\": false,\n"
76+
+ " \"is_wildcard\": false\n"
77+
+ " },\n"
78+
+ " {\n"
79+
+ " \"path\": \"java.util.Collections\",\n"
80+
+ " \"is_static\": true,\n"
81+
+ " \"is_wildcard\": true\n"
82+
+ " }\n"
83+
+ " ],\n"
84+
+ " \"type_declarations\": {},\n"
85+
+ " \"is_modified\": false\n"
86+
+ " }\n"
87+
+ " }\n"
88+
+ "}\n";
89+
Path analysisFilePath = writeAnalysisFile(jsonContent);
90+
91+
Map<String, JavaCompilationUnit> symbolTable = invokeReadSymbolTableFromFile(analysisFilePath);
92+
Assertions.assertNotNull(symbolTable);
93+
Assertions.assertEquals(1, symbolTable.size());
94+
95+
JavaCompilationUnit compilationUnit = symbolTable.get("/tmp/T.java");
96+
Assertions.assertNotNull(compilationUnit);
97+
List<Import> imports = compilationUnit.getImports();
98+
Assertions.assertNotNull(imports);
99+
Assertions.assertEquals(2, imports.size());
100+
101+
Import defaultImport = imports.stream()
102+
.filter(imp -> "java.util.List".equals(imp.getPath()))
103+
.findFirst()
104+
.orElse(null);
105+
Assertions.assertNotNull(defaultImport);
106+
Assertions.assertFalse(defaultImport.isStatic());
107+
Assertions.assertFalse(defaultImport.isWildcard());
108+
109+
Import staticWildcardImport = imports.stream()
110+
.filter(imp -> "java.util.Collections".equals(imp.getPath()))
111+
.findFirst()
112+
.orElse(null);
113+
Assertions.assertNotNull(staticWildcardImport);
114+
Assertions.assertTrue(staticWildcardImport.isStatic());
115+
Assertions.assertTrue(staticWildcardImport.isWildcard());
116+
}
117+
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.ibm.cldk;
22

3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import com.google.gson.JsonParser;
37
import com.ibm.cldk.entities.CallSite;
48
import com.ibm.cldk.entities.Callable;
9+
import com.ibm.cldk.entities.Import;
510
import com.ibm.cldk.entities.JavaCompilationUnit;
611
import com.ibm.cldk.entities.Type;
712
import java.io.BufferedReader;
@@ -85,4 +90,45 @@ public void testCallSiteArgumentExpression() throws IOException {
8590
}
8691
}
8792

93+
@Test
94+
public void testExtractSingleImportMetadata() throws IOException {
95+
String javaCode = String.join("\n",
96+
"import java.util.List;",
97+
"import java.util.Map.*;",
98+
"import static java.util.Collections.emptyList;",
99+
"import static java.util.Collections.*;",
100+
"class T {}");
101+
Map<String, JavaCompilationUnit> symbolTable = SymbolTable.extractSingle(javaCode).getLeft();
102+
Assertions.assertEquals(1, symbolTable.size());
103+
List<Import> imports = symbolTable.values().iterator().next().getImports();
104+
Assertions.assertNotNull(imports);
105+
Assertions.assertEquals(4, imports.size());
106+
107+
assertImport(imports, "java.util.List", false, false);
108+
assertImport(imports, "java.util.Map", false, true);
109+
assertImport(imports, "java.util.Collections.emptyList", true, false);
110+
assertImport(imports, "java.util.Collections", true, true);
111+
112+
JsonArray serializedImports = JsonParser.parseString(CodeAnalyzer.gson.toJson(imports)).getAsJsonArray();
113+
Assertions.assertEquals(4, serializedImports.size());
114+
for (JsonElement serializedImport : serializedImports) {
115+
Assertions.assertTrue(serializedImport.isJsonObject());
116+
JsonObject serializedImportObject = serializedImport.getAsJsonObject();
117+
Assertions.assertTrue(serializedImportObject.has("path"));
118+
Assertions.assertTrue(serializedImportObject.has("is_static"));
119+
Assertions.assertTrue(serializedImportObject.has("is_wildcard"));
120+
}
121+
}
122+
123+
private static void assertImport(List<Import> imports, String path, boolean isStatic, boolean isWildcard) {
124+
Import matchingImport = imports.stream()
125+
.filter(imp -> path.equals(imp.getPath())
126+
&& imp.isStatic() == isStatic
127+
&& imp.isWildcard() == isWildcard)
128+
.findFirst()
129+
.orElse(null);
130+
Assertions.assertNotNull(matchingImport,
131+
String.format("Expected import '%s' with isStatic=%s and isWildcard=%s", path, isStatic, isWildcard));
132+
}
133+
88134
}

0 commit comments

Comments
 (0)