ai idea
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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+", "-> ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user