Skip to content

Commit fee2161

Browse files
committed
feat: resolve symbols from JDK/dependency bytecode for native-image parity
The GraalVM native binary diverged from `java -jar`: empty callee signatures/return types and missing CRUD detection. Root cause is that in a native image `Class.forName` resolves a class but `getDeclaredMethods()`/`getDeclaredFields()` return empty for unregistered types, so a `ReflectionTypeSolver` silently "resolves" every lookup while enumerating zero members. Add `JmodTypeSolver`, a reflection-free JavaParser `TypeSolver` that reads `.class` bytecode straight out of zip archives (the JDK's `.jmod` files and the project's downloaded dependency jars) via a custom `javassist.ClassPath` backed by `ZipFile`. This works identically in the JVM and the native image. Wire it ahead of `ReflectionTypeSolver` in all three extract paths; keep `JarTypeSolver` only as a JVM fallback when no jmods are present. Supporting changes: - ScopeUtils.resolveJmodsDir() made public and honors a CODEANALYZER_JMODS_DIR override so the packaged distribution can point at bundled jmods instead of a host JAVA_HOME. - VersionProvider reads the version from an embedded resource (no JAR manifest exists at runtime in a native image). - Declare the native-image-config dir as a nativeCompile input so edits to reflect-/resource-config reliably trigger a rebuild. - Regenerate targeted reflect-/resource-config.json. Verified byte-identical native vs `java -jar` output (Anonymous-UUIDs masked) across call-graph-test (L2), record-class-test, init-blocks-test, plantsbywebsphere (CRUD 38=38), and daytrader8.
1 parent dbd9ac7 commit fee2161

8 files changed

Lines changed: 4475 additions & 257 deletions

File tree

build.gradle

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ compileJava.dependsOn spotlessApply
160160
// Optionally, automatically format before compilation
161161
// compileJava.dependsOn googleJavaFormat
162162

163+
// Bake the project version into an embedded resource so the native image (which
164+
// has no JAR manifest at runtime) can report it identically to `java -jar`.
165+
processResources {
166+
filesMatching('codeanalyzer-version.properties') {
167+
expand(version: project.version)
168+
}
169+
}
170+
163171
task fatJar(type: Jar) {
164172
archiveBaseName = 'codeanalyzer'
165173
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
@@ -193,7 +201,6 @@ graalvmNative {
193201
buildArgs.add("-Ob")
194202
buildArgs.add("-march=compatibility")
195203
buildArgs.add("--no-fallback")
196-
buildArgs.add("--no-server")
197204
buildArgs.add("-H:ReflectionConfigurationFiles=$projectDir/src/main/resources/META-INF/native-image-config/reflect-config.json")
198205
buildArgs.add("-H:ResourceConfigurationFiles=$projectDir/src/main/resources/META-INF/native-image-config/resource-config.json")
199206
buildArgs.add("-H:JNIConfigurationFiles=$projectDir/src/main/resources/META-INF/native-image-config/jni-config.json")
@@ -208,6 +215,14 @@ graalvmNative {
208215
}
209216
}
210217

218+
// The native-image config above is passed via -H flags as absolute source
219+
// paths, which Gradle does not track. Declare the directory as a task input so
220+
// editing reflect-/resource-/jni-/proxy-config.json reliably re-runs the build.
221+
tasks.named('nativeCompile').configure {
222+
inputs.dir("$projectDir/src/main/resources/META-INF/native-image-config")
223+
.withPropertyName('nativeImageConfig')
224+
}
225+
211226
// Define a property for the output directory
212227
def binDir = project.hasProperty('binDir') ? project.property('binDir') : "$projectDir/artifacts/bin"
213228

@@ -294,5 +309,5 @@ tasks.register('bumpVersion') {
294309

295310
nativeCompile.finalizedBy copyNativeExecutable
296311
kotlin {
297-
jvmToolchain(11)
312+
jvmToolchain(21)
298313
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
import java.io.FileReader;
2828
import java.io.FileWriter;
2929
import java.io.IOException;
30+
import java.io.InputStream;
3031
import java.lang.reflect.Type;
3132
import java.nio.file.Files;
3233
import java.nio.file.Path;
3334
import java.nio.file.Paths;
3435
import java.util.List;
3536
import java.util.Map;
37+
import java.util.Properties;
3638
import java.util.stream.Collectors;
3739
import org.apache.commons.lang3.tuple.Pair;
3840
import picocli.CommandLine;
@@ -42,6 +44,21 @@
4244
class VersionProvider implements CommandLine.IVersionProvider {
4345

4446
public String[] getVersion() throws Exception {
47+
// Read the version baked into an embedded resource at build time. The JAR
48+
// manifest's Implementation-Version is unavailable in a GraalVM native
49+
// image (no manifest at runtime), so the resource is the portable source.
50+
try (InputStream in = getClass().getResourceAsStream("/codeanalyzer-version.properties")) {
51+
if (in != null) {
52+
Properties props = new Properties();
53+
props.load(in);
54+
String v = props.getProperty("version");
55+
if (v != null && !v.isBlank() && !v.contains("${")) {
56+
return new String[] { v };
57+
}
58+
}
59+
} catch (IOException ignored) {
60+
// fall through to manifest lookup
61+
}
4562
String version = getClass().getPackage().getImplementationVersion();
4663
return new String[] { version != null ? version : "unknown" };
4764
}

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

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.github.javaparser.resolution.types.ResolvedType;
1919
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
2020
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
21+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
22+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
2123
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
2224
import com.github.javaparser.symbolsolver.utils.SymbolSolverCollectionStrategy;
2325
import com.github.javaparser.utils.ProjectRoot;
@@ -29,8 +31,10 @@
2931
import com.ibm.cldk.javaee.EntrypointsFinderFactory;
3032
import com.ibm.cldk.javaee.utils.enums.CRUDOperationType;
3133
import com.ibm.cldk.javaee.utils.enums.CRUDQueryType;
34+
import com.ibm.cldk.utils.JmodTypeSolver;
3235
import com.ibm.cldk.utils.Log;
3336
import java.io.IOException;
37+
import java.nio.file.Files;
3438
import java.nio.file.Path;
3539
import java.nio.file.Paths;
3640
import java.util.*;
@@ -1171,6 +1175,61 @@ private static boolean excludeSourceRoot(Path sourceRoot) {
11711175
return false;
11721176
}
11731177

1178+
/**
1179+
* Builds the JavaParser symbol solver used for project-mode analysis.
1180+
* Mirrors {@link SymbolSolverCollectionStrategy}'s composition (a
1181+
* {@code JavaParserTypeSolver} per source root + reflection + the project's
1182+
* dependency jars) but resolves JDK and dependency types from bytecode via a
1183+
* {@link JmodTypeSolver} instead of reflection / javassist's
1184+
* {@code JarTypeSolver}.
1185+
*
1186+
* <p>The bytecode solver precedes reflection because, in the native image,
1187+
* {@code Class.forName} succeeds for reachable JDK classes so the reflection
1188+
* solver "wins" the type lookup but then enumerates zero members
1189+
* (methods/fields aren't registered), leaving calls unresolved. Reading
1190+
* bytecode first yields identical results in the JVM and the native image.
1191+
* Dependency jars are indexed into the same bytecode solver rather than
1192+
* javassist's {@code JarTypeSolver}, whose built-in jar class path cannot
1193+
* read class bytes under native-image.
1194+
*/
1195+
private static JavaSymbolSolver buildProjectSymbolSolver(ProjectRoot projectRoot, Path projectRootPath,
1196+
ParserConfiguration config) {
1197+
CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver();
1198+
JmodTypeSolver jmodTypeSolver = JmodTypeSolver.tryCreate();
1199+
if (jmodTypeSolver != null) {
1200+
jmodTypeSolver.addJars(findDependencyJars(projectRootPath));
1201+
combinedTypeSolver.add(jmodTypeSolver);
1202+
}
1203+
combinedTypeSolver.add(new ReflectionTypeSolver());
1204+
for (SourceRoot sourceRoot : projectRoot.getSourceRoots()) {
1205+
if (excludeSourceRoot(sourceRoot.getRoot())) {
1206+
continue;
1207+
}
1208+
combinedTypeSolver.add(new JavaParserTypeSolver(sourceRoot.getRoot(), config));
1209+
}
1210+
// Fallback for environments without jmods (e.g. a JRE-only dev box):
1211+
// resolve dependency jars via javassist's JarTypeSolver under the JVM.
1212+
if (jmodTypeSolver == null) {
1213+
for (Path jar : findDependencyJars(projectRootPath)) {
1214+
try {
1215+
combinedTypeSolver.add(new JarTypeSolver(jar.toString()));
1216+
} catch (IOException e) {
1217+
Log.warn("Skipping unreadable jar for symbol resolution: " + jar + " (" + e.getMessage() + ")");
1218+
}
1219+
}
1220+
}
1221+
return new JavaSymbolSolver(combinedTypeSolver);
1222+
}
1223+
1224+
private static List<Path> findDependencyJars(Path projectRootPath) {
1225+
try (java.util.stream.Stream<Path> paths = Files.walk(projectRootPath)) {
1226+
return paths.filter(p -> p.toString().endsWith(".jar")).collect(Collectors.toList());
1227+
} catch (IOException e) {
1228+
Log.warn("Failed to scan for jars under " + projectRootPath + ": " + e.getMessage());
1229+
return Collections.emptyList();
1230+
}
1231+
}
1232+
11741233
/**
11751234
* Sets up lexical preserving printer for the given compilation unit in a safe manner by checking
11761235
* whether any node in the unit is missing ranges, which can result in exception.
@@ -1197,8 +1256,8 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
11971256
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21);
11981257
SymbolSolverCollectionStrategy symbolSolverCollectionStrategy = new SymbolSolverCollectionStrategy(config);
11991258
ProjectRoot projectRoot = symbolSolverCollectionStrategy.collect(projectRootPath);
1200-
javaSymbolSolver = (JavaSymbolSolver) symbolSolverCollectionStrategy.getParserConfiguration()
1201-
.getSymbolResolver().get();
1259+
javaSymbolSolver = buildProjectSymbolSolver(projectRoot, projectRootPath, config);
1260+
config.setSymbolResolver(javaSymbolSolver);
12021261
Map<String, JavaCompilationUnit> symbolTable = new LinkedHashMap<>();
12031262
Map<String, List<Problem>> parseProblems = new HashMap<>();
12041263
for (SourceRoot sourceRoot : projectRoot.getSourceRoots()) {
@@ -1223,8 +1282,14 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
12231282
throws IOException {
12241283
Map symbolTable = new LinkedHashMap<String, JavaCompilationUnit>();
12251284
Map parseProblems = new HashMap<String, List<Problem>>();
1226-
// Setting up symbol solvers
1285+
// Setting up symbol solvers. The jmod (bytecode) solver precedes the
1286+
// reflection solver so JDK types resolve identically in the JVM and the
1287+
// native image (see buildProjectSymbolSolver for the rationale).
12271288
CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver();
1289+
JmodTypeSolver jmodTypeSolver = JmodTypeSolver.tryCreate();
1290+
if (jmodTypeSolver != null) {
1291+
combinedTypeSolver.add(jmodTypeSolver);
1292+
}
12281293
combinedTypeSolver.add(new ReflectionTypeSolver());
12291294

12301295
ParserConfiguration parserConfiguration = new ParserConfiguration()
@@ -1259,15 +1324,15 @@ public static Pair<Map<String, JavaCompilationUnit>, Map<String, List<Problem>>>
12591324
List<Path> javaFilePaths) throws IOException {
12601325

12611326
// create symbol solver and parser configuration
1262-
SymbolSolverCollectionStrategy symbolSolverCollectionStrategy = new SymbolSolverCollectionStrategy();
1263-
ProjectRoot projectRoot = symbolSolverCollectionStrategy.collect(projectRootPath);
1264-
javaSymbolSolver = (JavaSymbolSolver) symbolSolverCollectionStrategy.getParserConfiguration()
1265-
.getSymbolResolver().get();
12661327
Log.info("Setting parser language level to JAVA_21");
12671328
ParserConfiguration parserConfiguration = new ParserConfiguration()
12681329
.setStoreTokens(true)
12691330
.setAttributeComments(true)
12701331
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21);
1332+
SymbolSolverCollectionStrategy symbolSolverCollectionStrategy =
1333+
new SymbolSolverCollectionStrategy(parserConfiguration);
1334+
ProjectRoot projectRoot = symbolSolverCollectionStrategy.collect(projectRootPath);
1335+
javaSymbolSolver = buildProjectSymbolSolver(projectRoot, projectRootPath, parserConfiguration);
12711336
parserConfiguration.setSymbolResolver(javaSymbolSolver);
12721337

12731338
// create java parser with the configuration

0 commit comments

Comments
 (0)