ai idea
This commit is contained in:
@@ -27,10 +27,29 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (expr == null) return null;
|
if (expr == null) return null;
|
||||||
return parsedNodeCache.computeIfAbsent(expr, e -> {
|
return parsedNodeCache.computeIfAbsent(expr, e -> {
|
||||||
ASTParser parser = ASTParser.newParser(AST.JLS17);
|
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.setKind(ASTParser.K_EXPRESSION);
|
||||||
parser.setSource(e.toCharArray());
|
parser.setSource(e.toCharArray());
|
||||||
try {
|
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) {
|
} catch (Exception ex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -364,6 +383,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ASTNode exprNode = parseExpressionString(resolvedValue);
|
ASTNode exprNode = parseExpressionString(resolvedValue);
|
||||||
|
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||||
|
exprNode = pe.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
String varName = null;
|
String varName = null;
|
||||||
String methodName = null;
|
String methodName = null;
|
||||||
@@ -495,6 +517,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
sourceMethod = "inline-ternary";
|
sourceMethod = "inline-ternary";
|
||||||
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
|
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) {
|
} else if (exprNode instanceof SimpleName sn) {
|
||||||
varName = sn.getIdentifier();
|
varName = sn.getIdentifier();
|
||||||
if (varName.equals(varName.toUpperCase())) {
|
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) {
|
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
ASTNode exprNode = parseExpressionString(resolvedValue);
|
ASTNode exprNode = parseExpressionString(resolvedValue);
|
||||||
|
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||||
|
exprNode = pe.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
if (exprNode instanceof SuperMethodInvocation smi) {
|
if (exprNode instanceof SuperMethodInvocation smi) {
|
||||||
String getterName = smi.getName().getIdentifier();
|
String getterName = smi.getName().getIdentifier();
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ public class DynamicClasspathResolver {
|
|||||||
|
|
||||||
protected List<String> resolveMaven(Path projectRoot) {
|
protected List<String> resolveMaven(Path projectRoot) {
|
||||||
log.info("Maven project detected. Resolving dependencies...");
|
log.info("Maven project detected. Resolving dependencies...");
|
||||||
Path cpFile = null;
|
|
||||||
try {
|
try {
|
||||||
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
|
|
||||||
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
|
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
|
||||||
if (mvnCmd == null) {
|
if (mvnCmd == null) {
|
||||||
log.warn("Maven executable not found.");
|
log.warn("Maven executable not found.");
|
||||||
@@ -48,26 +46,32 @@ public class DynamicClasspathResolver {
|
|||||||
"-q",
|
"-q",
|
||||||
"dependency:build-classpath",
|
"dependency:build-classpath",
|
||||||
"-DincludeScope=compile",
|
"-DincludeScope=compile",
|
||||||
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
|
"-Dmdep.outputFile=sme_cp.txt"
|
||||||
);
|
);
|
||||||
pb.directory(projectRoot.toFile());
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
runProcess(pb);
|
runProcess(pb);
|
||||||
|
|
||||||
if (Files.exists(cpFile)) {
|
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();
|
String cpString = Files.readString(cpFile).trim();
|
||||||
if (!cpString.isEmpty()) {
|
if (!cpString.isEmpty()) {
|
||||||
return Arrays.asList(cpString.split(File.pathSeparator));
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to dynamically resolve Maven classpath", e);
|
log.error("Failed to dynamically resolve Maven classpath", e);
|
||||||
} finally {
|
|
||||||
if (cpFile != null) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(cpFile);
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -107,14 +111,18 @@ public class DynamicClasspathResolver {
|
|||||||
pb.directory(projectRoot.toFile());
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
String output = runProcessAndGetOutput(pb);
|
String output = runProcessAndGetOutput(pb);
|
||||||
|
java.util.Set<String> allPaths = new java.util.LinkedHashSet<>();
|
||||||
for (String line : output.split("\\r?\\n")) {
|
for (String line : output.split("\\r?\\n")) {
|
||||||
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
|
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
|
||||||
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
|
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
|
||||||
if (!cpString.isEmpty()) {
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to dynamically resolve Gradle classpath", e);
|
log.error("Failed to dynamically resolve Gradle classpath", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -162,7 +162,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to base class for lambda, method reference, and constructor unwrapping
|
// 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) {
|
protected String resolveCalledMethod(MethodInvocation node) {
|
||||||
|
|||||||
@@ -110,12 +110,44 @@ public class VariableTracer {
|
|||||||
if (superFqn != null) {
|
if (superFqn != null) {
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
if (superTd != null) {
|
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;
|
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) {
|
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
||||||
if (methodFqn == null) return null;
|
if (methodFqn == null) return null;
|
||||||
int lastDot = methodFqn.lastIndexOf('.');
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
@@ -294,14 +326,16 @@ public class VariableTracer {
|
|||||||
stringified.add(traced.toString());
|
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();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < stringified.size() - 1; i++) {
|
for (int i = 0; i < stringified.size() - 1; i++) {
|
||||||
sb.append("true ? ").append(stringified.get(i)).append(" : ");
|
sb.append("true ? ").append(stringified.get(i)).append(" : ");
|
||||||
}
|
}
|
||||||
sb.append(stringified.get(stringified.size() - 1));
|
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="))
|
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.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);
|
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).hasSize(1);
|
||||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("REACTIVE_EVENT");
|
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