3 Commits

Author SHA1 Message Date
89736a4aa1 4 2026-07-06 17:59:58 +02:00
092fbe49fa 4 2026-07-06 17:56:19 +02:00
61f5549589 3 2026-07-06 17:53:42 +02:00
4 changed files with 147 additions and 2 deletions

View File

@@ -238,6 +238,20 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
}
if (!found) {
// Bridge polymorphic interfaces to implementations without graph edges
String callerSimple = caller.contains(".") ? caller.substring(caller.lastIndexOf('.') + 1) : caller;
String targetSimple = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
if (callerSimple.equals(targetSimple)) {
String interfaceParamName = typeResolver.getParameterName(caller, paramIndex);
if (interfaceParamName != null) {
currentParamName = interfaceParamName;
if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName;
resolvedValue = currentParamName + methodSuffix;
found = true;
}
}
}
if (!found) break;
}

View File

@@ -30,6 +30,21 @@ public class TypeResolver {
return -1;
}
public String getParameterName(String methodFqn, int index) {
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && index < md.parameters().size()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index);
return svd.getName().getIdentifier();
}
}
return null;
}
public String getParameterType(String methodFqn, int index) {
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));

View File

@@ -413,6 +413,33 @@ public class VariableTracer {
public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
Map<String, String> paramValues = new HashMap<>();
List<CallEdge> edges = callGraph.get(caller);
boolean hasEdge = false;
if (edges != null) {
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
hasEdge = true;
break;
}
}
}
// Bridge polymorphic interfaces to implementations without graph edges
if (!hasEdge) {
String callerSimple = caller.contains(".") ? caller.substring(caller.lastIndexOf('.') + 1) : caller;
String targetSimple = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
if (callerSimple.equals(targetSimple)) {
List<String> callerParams = getParameterNames(caller);
List<String> targetParams = getParameterNames(target);
if (callerParams != null && targetParams != null && callerParams.size() == targetParams.size()) {
for (int i = 0; i < callerParams.size(); i++) {
paramValues.put(targetParams.get(i), callerParams.get(i));
}
return paramValues;
}
}
}
if (edges == null) {
return paramValues;
}
@@ -453,15 +480,35 @@ public class VariableTracer {
return paramValues;
}
private List<String> getParameterNames(String methodFqn) {
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String className = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) return null;
List<String> paramNames = new ArrayList<>();
for (Object paramObj : md.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
return paramNames;
}
public String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
if (argValue == null) return null;
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) {
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*") || (argValue.startsWith("\"") && argValue.endsWith("\""))) {
return argValue;
}
String tracedLocal = traceLocalVariable(callerMethod, argValue);
if (tracedLocal != null && !tracedLocal.equals(argValue)) {
if (tracedLocal.contains(".") || tracedLocal.startsWith("new ") || tracedLocal.matches(".*[0-9].*") || tracedLocal.contains("(")) {
if (tracedLocal.contains(".") || tracedLocal.startsWith("new ") || tracedLocal.matches(".*[0-9].*") || tracedLocal.contains("(") || (tracedLocal.startsWith("\"") && tracedLocal.endsWith("\""))) {
return tracedLocal;
}
argValue = tracedLocal;

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class VariableTracerTest {
@Test
void shouldShortCircuitLiteralStringResolution() {
CodebaseContext context = new CodebaseContext();
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
// Even with an empty context and call graph, passing a raw String literal should return instantly
// without trying to recursively traverse the path or crash.
String resolved = tracer.resolveArgumentValue("\"PAY_EVENT\"", "com.example.Order.process", List.of("com.example.Order.process"), 0, Map.of());
assertThat(resolved).isEqualTo("\"PAY_EVENT\"");
}
@Test
void shouldBridgePolymorphicParametersPositionally(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
// Setup an interface and implementation with DIFFERENT parameter names
Files.writeString(comFoo.resolve("PaymentService.java"),
"package com.foo;\n" +
"public interface PaymentService {\n" +
" void submit(String eventName);\n" +
"}"
);
Files.writeString(comFoo.resolve("PaymentServiceImpl.java"),
"package com.foo;\n" +
"public class PaymentServiceImpl implements PaymentService {\n" +
" @Override\n" +
" public void submit(String e) {}\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
// Simulate tracing from the Interface backwards to the Implementation.
// Because this is a polymorphic bridge, there is NO direct call edge in the AST call graph.
Map<String, String> params = tracer.buildParameterValuesMap(
"com.foo.PaymentService.submit",
"com.foo.PaymentServiceImpl.submit",
Map.of(), // Empty call graph (no direct edges)
List.of("com.foo.PaymentService.submit", "com.foo.PaymentServiceImpl.submit"),
0
);
// The tracer should have scanned both ASTs and mapped parameter 0 -> parameter 0
assertThat(params).hasSize(1);
assertThat(params).containsEntry("e", "eventName");
}
}