Skip to content

Commit 5c5ed93

Browse files
committed
Improve accuracy
1 parent f491d22 commit 5c5ed93

3 files changed

Lines changed: 174 additions & 78 deletions

File tree

src/main/java/com/neuvem/java2graph/passes/ParsePass.java

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@
22

33
import com.github.javaparser.JavaParser;
44
import com.github.javaparser.ParserConfiguration;
5+
import com.github.javaparser.ParseResult;
56
import com.github.javaparser.ast.CompilationUnit;
7+
import com.github.javaparser.ast.body.TypeDeclaration;
68
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
79
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
810
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
9-
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
1011
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
12+
import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver;
1113
import com.neuvem.java2graph.Java2GraphConfig;
1214
import com.neuvem.java2graph.models.GraphContext;
13-
import com.github.javaparser.ParseResult;
1415

1516
import java.io.IOException;
17+
import java.nio.MappedByteBuffer;
18+
import java.nio.channels.FileChannel;
19+
import java.nio.charset.StandardCharsets;
1620
import java.nio.file.Files;
1721
import java.nio.file.Path;
22+
import java.nio.file.StandardOpenOption;
1823
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Optional;
26+
import java.util.concurrent.ConcurrentHashMap;
1927
import java.util.stream.Collectors;
2028
import java.util.stream.Stream;
2129

@@ -26,51 +34,75 @@ public class ParsePass implements Pass {
2634

2735
@Override
2836
public void execute(Java2GraphConfig config, GraphContext context) throws Exception {
29-
System.out.println("Configuring Symbol Solver...");
30-
31-
CombinedTypeSolver typeSolver = new CombinedTypeSolver();
32-
33-
// 1. ReflectionTypeSolver for classes on the full classpath (not just JRE)
34-
typeSolver.add(new ReflectionTypeSolver(false));
37+
System.out.println("Beginning Two-Pass Parsing...");
3538

36-
// 2. JavaParserTypeSolver for source code being analyzed
37-
typeSolver.add(new JavaParserTypeSolver(config.getSrcDir()));
38-
39-
if (config.getJarPaths() != null) {
40-
for (Path jarPath : config.getJarPaths()) {
41-
scanAndAddJars(typeSolver, jarPath);
42-
}
43-
}
44-
45-
context.typeSolver = typeSolver;
46-
JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver);
47-
48-
// Configure JavaParser with the Symbol Solver
49-
ParserConfiguration parserConfiguration = new ParserConfiguration()
50-
.setSymbolResolver(symbolSolver)
51-
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17);
52-
5339
List<Path> javaFiles;
5440
try (Stream<Path> paths = Files.walk(config.getSrcDir())) {
5541
javaFiles = paths.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
5642
}
5743

58-
System.out.println("Parsing " + javaFiles.size() + " files...");
44+
// Pass 1: Parse all files WITHOUT the symbol solver to build the index
45+
// This avoids "cold start" resolution failures during parallel parsing.
46+
System.out.println("Pass 1: Parsing " + javaFiles.size() + " files...");
47+
48+
ParserConfiguration initialConfig = new ParserConfiguration()
49+
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17)
50+
.setStoreTokens(true) // Restore token storage for AST completeness
51+
.setAttributeComments(true);
52+
53+
Map<String, CompilationUnit> cuIndex = new ConcurrentHashMap<>();
5954

60-
javaFiles.parallelStream().forEach(path -> {
55+
javaFiles.parallelStream().forEach((Path path) -> {
6156
try {
62-
// Instantiate a new JavaParser per thread
63-
JavaParser javaParser = new JavaParser(parserConfiguration);
57+
com.github.javaparser.JavaParser javaParser = new com.github.javaparser.JavaParser(initialConfig);
58+
// Standard parser call (handles encoding and storage metadata)
6459
ParseResult<CompilationUnit> result = javaParser.parse(path);
6560
result.getResult().ifPresent(cu -> {
6661
context.compilationUnits.put(path.toString(), cu);
62+
63+
cu.findAll(TypeDeclaration.class).forEach(td -> {
64+
Optional<String> fqnOpt = ((TypeDeclaration<?>)td).getFullyQualifiedName();
65+
if (fqnOpt.isPresent()) {
66+
cuIndex.put(fqnOpt.get(), cu);
67+
}
68+
});
6769
});
6870
} catch (Throwable e) {
6971
System.err.println("Failed to parse: " + path + " - " + e.getMessage());
7072
}
7173
});
74+
75+
System.out.println("Pass 2: Configuring Symbol Resolver with " + cuIndex.size() + " indexed types...");
76+
77+
CombinedTypeSolver typeSolver = new CombinedTypeSolver();
78+
79+
// 1. SourceMemoryTypeSolver MUST BE FIRST to avoid shadowing by JARs or JRE
80+
typeSolver.add(new SourceMemoryTypeSolver(cuIndex));
81+
82+
// 2. ReflectionTypeSolver for standard JRE classes
83+
typeSolver.add(new ReflectionTypeSolver(true));
7284

73-
System.out.println("Finished parsing.");
85+
// 3. ClassLoaderTypeSolver for the current thread classloader
86+
typeSolver.add(new ClassLoaderTypeSolver(Thread.currentThread().getContextClassLoader()));
87+
88+
// 4. Fallback reflection (includes app classpath)
89+
typeSolver.add(new ReflectionTypeSolver(false));
90+
91+
if (config.getJarPaths() != null) {
92+
for (Path jarPath : config.getJarPaths()) {
93+
scanAndAddJars(typeSolver, jarPath);
94+
}
95+
}
96+
97+
context.typeSolver = typeSolver;
98+
JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver);
99+
100+
// Attach the resolver to all CompilationUnits for the resolution pass
101+
context.compilationUnits.values().forEach(cu -> {
102+
cu.setData(com.github.javaparser.ast.Node.SYMBOL_RESOLVER_KEY, symbolSolver);
103+
});
104+
105+
System.out.println("Finished two-pass parsing and resolver configuration.");
74106
}
75107

76108
private void scanAndAddJars(CombinedTypeSolver typeSolver, Path path) throws IOException {

src/main/java/com/neuvem/java2graph/passes/ResolvePass.java

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ public void execute(Java2GraphConfig config, GraphContext context) throws Except
3131
try {
3232
cu.accept(new ResolverVisitor(context, cu), null);
3333
} catch (Throwable e) {
34-
System.err.println("Failed to resolve compilation unit: " + e.getMessage());
34+
System.err.println("Failed to resolve compilation unit: " + e.getClass().getSimpleName() + ": " + e.getMessage());
35+
if (e.getMessage() == null) {
36+
e.printStackTrace();
37+
}
3538
}
3639
});
3740

@@ -128,54 +131,58 @@ private static Map<String, Map<String, String>> buildImportMaps(CompilationUnit
128131

129132
private void addCall(String calledFqn) {
130133
if (currentMethodFqn != null && calledFqn != null) {
131-
// Ensure every call target has a Method node in our registry (parity with Joern)
132-
context.methods.computeIfAbsent(calledFqn, k -> {
133-
String workingFqn = k;
134-
String baseFqn = workingFqn;
135-
String signature = "()";
136-
if (workingFqn.contains("(")) {
137-
baseFqn = workingFqn.substring(0, workingFqn.indexOf('('));
138-
signature = workingFqn.substring(workingFqn.indexOf('('));
139-
}
140-
141-
String classFqn = "UNKNOWN";
142-
String name = baseFqn;
143-
if (baseFqn.startsWith("<unresolvedNamespace>.")) {
144-
classFqn = "<unresolvedNamespace>";
145-
name = baseFqn.substring("<unresolvedNamespace>.".length());
146-
} else if (baseFqn.startsWith(".")) {
147-
// Fix for dot-prefixed methods: redirect to unresolvedNamespace
148-
classFqn = "<unresolvedNamespace>";
149-
name = baseFqn.substring(1);
150-
workingFqn = classFqn + "." + name + signature;
151-
} else if (baseFqn.contains(".")) {
152-
classFqn = baseFqn.substring(0, baseFqn.lastIndexOf('.'));
153-
name = baseFqn.substring(baseFqn.lastIndexOf('.') + 1);
134+
try {
135+
// Ensure every call target has a Method node in our registry (parity with Joern)
136+
context.methods.computeIfAbsent(calledFqn, k -> {
137+
String workingFqn = k;
138+
String baseFqn = workingFqn;
139+
String signature = "()";
140+
if (workingFqn.contains("(")) {
141+
baseFqn = workingFqn.substring(0, workingFqn.indexOf('('));
142+
signature = workingFqn.substring(workingFqn.indexOf('('));
143+
}
144+
145+
String classFqn = "UNKNOWN";
146+
String name = baseFqn;
147+
if (baseFqn.startsWith("<unresolvedNamespace>.")) {
148+
classFqn = "<unresolvedNamespace>";
149+
name = baseFqn.substring("<unresolvedNamespace>.".length());
150+
} else if (baseFqn.startsWith(".")) {
151+
// Fix for dot-prefixed methods: redirect to unresolvedNamespace
152+
classFqn = "<unresolvedNamespace>";
153+
name = baseFqn.substring(1);
154+
workingFqn = classFqn + "." + name + signature;
155+
} else if (baseFqn.contains(".")) {
156+
classFqn = baseFqn.substring(0, baseFqn.lastIndexOf('.'));
157+
name = baseFqn.substring(baseFqn.lastIndexOf('.') + 1);
158+
}
159+
160+
// Register a placeholder ClassNode for the containing class if it doesn't exist
161+
final String finalClassFqn = classFqn;
162+
context.classes.computeIfAbsent(finalClassFqn, cfqn -> ClassNode.builder()
163+
.id(cfqn).fqn(cfqn).name(cfqn.contains(".") ? cfqn.substring(cfqn.lastIndexOf('.') + 1) : cfqn)
164+
.isInterface(false).declarationCode("// referenced external/synthetic class")
165+
.build());
166+
167+
return MethodNode.builder()
168+
.id(workingFqn).fqn(workingFqn)
169+
.name(name)
170+
.signature(signature)
171+
.sourceCode("// referenced external/synthetic method")
172+
.containingClassFqn(finalClassFqn)
173+
.isLambda(workingFqn.contains("<lambda>"))
174+
.build();
175+
});
176+
177+
String edgeKey = currentMethodFqn + "→" + calledFqn;
178+
if (seenEdges.add(edgeKey)) {
179+
context.callEdges.add(MethodCallEdge.builder()
180+
.callerMethodFqn(currentMethodFqn)
181+
.calledMethodFqn(calledFqn)
182+
.build());
154183
}
155-
156-
// Register a placeholder ClassNode for the containing class if it doesn't exist
157-
final String finalClassFqn = classFqn;
158-
context.classes.computeIfAbsent(finalClassFqn, cfqn -> ClassNode.builder()
159-
.id(cfqn).fqn(cfqn).name(cfqn.contains(".") ? cfqn.substring(cfqn.lastIndexOf('.') + 1) : cfqn)
160-
.isInterface(false).declarationCode("// referenced external/synthetic class")
161-
.build());
162-
163-
return MethodNode.builder()
164-
.id(workingFqn).fqn(workingFqn)
165-
.name(name)
166-
.signature(signature)
167-
.sourceCode("// referenced external/synthetic method")
168-
.containingClassFqn(finalClassFqn)
169-
.isLambda(workingFqn.contains("<lambda>"))
170-
.build();
171-
});
172-
173-
String edgeKey = currentMethodFqn + "→" + calledFqn;
174-
if (seenEdges.add(edgeKey)) {
175-
context.callEdges.add(MethodCallEdge.builder()
176-
.callerMethodFqn(currentMethodFqn)
177-
.calledMethodFqn(calledFqn)
178-
.build());
184+
} catch (Exception e) {
185+
System.err.println("Warning: failed to add call edge from " + currentMethodFqn + " to " + calledFqn + ": " + e.getMessage());
179186
}
180187
}
181188
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.neuvem.java2graph.passes;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import com.github.javaparser.ast.body.TypeDeclaration;
5+
import com.github.javaparser.resolution.TypeSolver;
6+
import com.github.javaparser.resolution.model.SymbolReference;
7+
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
8+
import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade;
9+
10+
import java.util.Map;
11+
import java.util.Optional;
12+
13+
/**
14+
* A custom TypeSolver that uses pre-parsed CompilationUnits from a map.
15+
* This avoids re-parsing files from disk during symbol resolution.
16+
*/
17+
public class SourceMemoryTypeSolver implements TypeSolver {
18+
19+
private TypeSolver parent;
20+
private final Map<String, CompilationUnit> classToIndex;
21+
22+
public SourceMemoryTypeSolver(Map<String, CompilationUnit> classToIndex) {
23+
this.classToIndex = classToIndex;
24+
}
25+
26+
@Override
27+
public TypeSolver getParent() {
28+
return parent;
29+
}
30+
31+
@Override
32+
public void setParent(TypeSolver parent) {
33+
this.parent = parent;
34+
}
35+
36+
@Override
37+
public SymbolReference<ResolvedReferenceTypeDeclaration> tryToSolveType(String name) {
38+
CompilationUnit cu = classToIndex.get(name);
39+
if (cu != null) {
40+
// Find the type declaration within the CU that matches the FQN (includes inner/nested types)
41+
Optional<TypeDeclaration<?>> typeDeclaration = cu.findAll(TypeDeclaration.class).stream()
42+
.filter(t -> {
43+
Optional<String> fqn = ((TypeDeclaration<?>) t).getFullyQualifiedName();
44+
return fqn.isPresent() && fqn.get().equals(name);
45+
})
46+
.findFirst()
47+
.map(t -> (TypeDeclaration<?>)t);
48+
49+
if (typeDeclaration.isPresent()) {
50+
return SymbolReference.solved(JavaParserFacade.get(this).getTypeDeclaration(typeDeclaration.get()));
51+
}
52+
}
53+
54+
// If not found in our index, we can't solve it
55+
return SymbolReference.unsolved(ResolvedReferenceTypeDeclaration.class);
56+
}
57+
}

0 commit comments

Comments
 (0)