This commit is contained in:
2026-07-07 04:50:18 +02:00
parent a7bf07e8b2
commit 9a5f122bd2
6 changed files with 165 additions and 21 deletions

View File

@@ -27,10 +27,29 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (expr == null) return null;
return parsedNodeCache.computeIfAbsent(expr, e -> {
ASTParser parser = ASTParser.newParser(AST.JLS17);
Map<String, String> options = org.eclipse.jdt.core.JavaCore.getOptions();
org.eclipse.jdt.core.JavaCore.setComplianceOptions(org.eclipse.jdt.core.JavaCore.VERSION_17, options);
parser.setCompilerOptions(options);
parser.setKind(ASTParser.K_EXPRESSION);
parser.setSource(e.toCharArray());
try {
return parser.createAST(null);
ASTNode node = parser.createAST(null);
if (node instanceof org.eclipse.jdt.core.dom.CompilationUnit) {
ASTParser fallbackParser = ASTParser.newParser(AST.JLS17);
fallbackParser.setCompilerOptions(options);
fallbackParser.setKind(ASTParser.K_COMPILATION_UNIT);
fallbackParser.setSource(("class A { Object o = " + e + "; }").toCharArray());
org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) fallbackParser.createAST(null);
if (!cu.types().isEmpty()) {
org.eclipse.jdt.core.dom.TypeDeclaration td = (org.eclipse.jdt.core.dom.TypeDeclaration) cu.types().get(0);
if (!td.bodyDeclarations().isEmpty() && td.bodyDeclarations().get(0) instanceof org.eclipse.jdt.core.dom.FieldDeclaration fd) {
if (!fd.fragments().isEmpty() && fd.fragments().get(0) instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf) {
return vdf.getInitializer();
}
}
}
}
return node;
} catch (Exception ex) {
return null;
}
@@ -364,6 +383,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
ASTNode exprNode = parseExpressionString(resolvedValue);
while (exprNode instanceof ParenthesizedExpression pe) {
exprNode = pe.getExpression();
}
String varName = null;
String methodName = null;
@@ -495,6 +517,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
sourceMethod = "inline-ternary";
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof SwitchExpression swExpr) {
List<String> polyEventsRef = polymorphicEvents;
swExpr.accept(new ASTVisitor() {
@Override
public boolean visit(YieldStatement ys) {
List<Expression> tracedBranch = variableTracer.traceVariableAll(ys.getExpression());
for (Expression tb : tracedBranch) {
if (tb instanceof ClassInstanceCreation cic) {
polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
} else {
constantExtractor.extractConstantsFromExpression(tb, polyEventsRef);
}
}
return super.visit(ys);
}
@Override
public boolean visit(ExpressionStatement es) {
List<Expression> tracedBranch = variableTracer.traceVariableAll(es.getExpression());
for (Expression tb : tracedBranch) {
if (tb instanceof ClassInstanceCreation cic) {
polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
} else {
constantExtractor.extractConstantsFromExpression(tb, polyEventsRef);
}
}
return super.visit(es);
}
});
sourceMethod = "inline-switch";
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof SimpleName sn) {
varName = sn.getIdentifier();
if (varName.equals(varName.toUpperCase())) {
@@ -980,6 +1032,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
ASTNode exprNode = parseExpressionString(resolvedValue);
while (exprNode instanceof ParenthesizedExpression pe) {
exprNode = pe.getExpression();
}
if (exprNode instanceof SuperMethodInvocation smi) {
String getterName = smi.getName().getIdentifier();

View File

@@ -34,9 +34,7 @@ public class DynamicClasspathResolver {
protected List<String> resolveMaven(Path projectRoot) {
log.info("Maven project detected. Resolving dependencies...");
Path cpFile = null;
try {
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
if (mvnCmd == null) {
log.warn("Maven executable not found.");
@@ -48,26 +46,32 @@ public class DynamicClasspathResolver {
"-q",
"dependency:build-classpath",
"-DincludeScope=compile",
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
"-Dmdep.outputFile=sme_cp.txt"
);
pb.directory(projectRoot.toFile());
runProcess(pb);
if (Files.exists(cpFile)) {
String cpString = Files.readString(cpFile).trim();
if (!cpString.isEmpty()) {
return Arrays.asList(cpString.split(File.pathSeparator));
}
java.util.Set<String> allPaths = new java.util.LinkedHashSet<>();
try (java.util.stream.Stream<Path> stream = Files.walk(projectRoot)) {
stream.filter(p -> p.getFileName().toString().equals("sme_cp.txt"))
.forEach(cpFile -> {
try {
String cpString = Files.readString(cpFile).trim();
if (!cpString.isEmpty()) {
allPaths.addAll(Arrays.asList(cpString.split(File.pathSeparator)));
}
Files.deleteIfExists(cpFile);
} catch (IOException e) {
log.warn("Failed to read or delete sme_cp.txt at {}", cpFile, e);
}
});
}
if (!allPaths.isEmpty()) {
return new ArrayList<>(allPaths);
}
} catch (Exception e) {
log.error("Failed to dynamically resolve Maven classpath", e);
} finally {
if (cpFile != null) {
try {
Files.deleteIfExists(cpFile);
} catch (IOException ignored) {}
}
}
return Collections.emptyList();
}
@@ -107,14 +111,18 @@ public class DynamicClasspathResolver {
pb.directory(projectRoot.toFile());
String output = runProcessAndGetOutput(pb);
java.util.Set<String> allPaths = new java.util.LinkedHashSet<>();
for (String line : output.split("\\r?\\n")) {
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
if (!cpString.isEmpty()) {
return Arrays.asList(cpString.split(File.pathSeparator));
allPaths.addAll(Arrays.asList(cpString.split(File.pathSeparator)));
}
}
}
if (!allPaths.isEmpty()) {
return new ArrayList<>(allPaths);
}
} catch (Exception e) {
log.error("Failed to dynamically resolve Gradle classpath", e);
} finally {

View File

@@ -162,7 +162,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
}
// Delegate to base class for lambda, method reference, and constructor unwrapping
return super.resolveArgument(expr);
String result = super.resolveArgument(expr);
if (result != null) {
result = result.replaceAll("->\\s*yield\\s+", "-> ");
}
return result;
}
protected String resolveCalledMethod(MethodInvocation node) {

View File

@@ -110,11 +110,43 @@ public class VariableTracer {
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
return getFieldType(superTd, fieldName, context, visited);
String result = getFieldType(superTd, fieldName, context, visited);
if (result != null) return result;
}
}
for (Object intfObj : td.superInterfaceTypes()) {
Type intfType = (Type) intfObj;
String intfName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(intfType);
String resolvedIntf = resolveTypeFqn(intfName, context, td);
if (resolvedIntf != null) {
TypeDeclaration intfTd = context.getTypeDeclaration(resolvedIntf);
if (intfTd != null) {
String result = getFieldType(intfTd, fieldName, context, visited);
if (result != null) return result;
}
}
}
return null;
}
private String resolveTypeFqn(String simpleName, CodebaseContext context, TypeDeclaration td) {
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
if (cu != null) {
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String name = imp.getName().getFullyQualifiedName();
if (name.endsWith("." + simpleName)) {
return name;
}
}
if (cu.getPackage() != null) {
return cu.getPackage().getName().getFullyQualifiedName() + "." + simpleName;
}
}
return simpleName;
}
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
if (methodFqn == null) return null;
@@ -294,14 +326,16 @@ public class VariableTracer {
stringified.add(traced.toString());
}
}
if (stringified.size() == 1) return stringified.get(0);
if (stringified.size() == 1) {
return stringified.get(0).replaceAll("->\\s*yield\\s+", "-> ");
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < stringified.size() - 1; i++) {
sb.append("true ? ").append(stringified.get(i)).append(" : ");
}
sb.append(stringified.get(stringified.size() - 1));
return sb.toString();
return sb.toString().replaceAll("->\\s*yield\\s+", "-> ");
}
}

View File

@@ -31,7 +31,7 @@ class DynamicClasspathResolverTest {
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
.findFirst()
.orElseThrow();
Path outputFile = Path.of(outputFileArg.substring("-Dmdep.outputFile=".length()));
Path outputFile = pb.directory().toPath().resolve(outputFileArg.substring("-Dmdep.outputFile=".length()));
Files.writeString(outputFile, cp);
}

View File

@@ -1036,4 +1036,47 @@ class HeuristicCallGraphEngineCoreTest {
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("REACTIVE_EVENT");
}
@Test
void shouldExtractEventFromSwitchExpressionAndParentheses(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class SwitchOrderService {
public void processOrder(int type) {
String e = (((switch(type) {
case 1 -> "PAY";
default -> { yield "CANCEL"; }
})));
sendEvent(e);
}
public void sendEvent(String event) {
}
}
""";
Files.writeString(tempDir.resolve("SwitchOrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.SwitchOrderService")
.methodName("processOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.SwitchOrderService")
.methodName("sendEvent")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("POLY EVENTS: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("PAY", "CANCEL");
}
}