Fix Runnable lambda and rich-event predicate linking gaps.
Trace execute→Runnable.run arguments via source AST and JdtDataFlowModel void side-effects, and narrow polymorphic events using domain isCorrect() predicates on rich event types. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -247,7 +247,7 @@ public final class MachineEnumCanonicalizer {
|
||||
return polymorphicEvents;
|
||||
}
|
||||
return narrowPolymorphicCandidates(
|
||||
polymorphicEvents, constraint, eventTypeFqn, machineTransitions, context);
|
||||
polymorphicEvents, constraint, eventTypeFqn, machineTransitions, context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,7 +303,8 @@ public final class MachineEnumCanonicalizer {
|
||||
expanded.getConstraint(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
machineTransitions,
|
||||
context);
|
||||
context,
|
||||
expanded);
|
||||
if (!narrowed.equals(expanded.getPolymorphicEvents())) {
|
||||
return expanded.toBuilder()
|
||||
.polymorphicEvents(narrowed)
|
||||
@@ -317,7 +318,8 @@ public final class MachineEnumCanonicalizer {
|
||||
expanded.getConstraint(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
machineTransitions,
|
||||
context);
|
||||
context,
|
||||
expanded);
|
||||
if (!machineEvents.isEmpty()) {
|
||||
return expanded.toBuilder()
|
||||
.polymorphicEvents(machineEvents)
|
||||
@@ -414,7 +416,8 @@ public final class MachineEnumCanonicalizer {
|
||||
String constraint,
|
||||
String eventTypeFqn,
|
||||
List<Transition> machineTransitions,
|
||||
CodebaseContext context) {
|
||||
CodebaseContext context,
|
||||
TriggerPoint trigger) {
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
|
||||
if (transitionEvents.isEmpty()) {
|
||||
return List.of();
|
||||
@@ -425,6 +428,13 @@ public final class MachineEnumCanonicalizer {
|
||||
if (!filtered.isEmpty()) {
|
||||
return filtered;
|
||||
}
|
||||
if (trigger != null) {
|
||||
List<String> richFiltered = RichEventPredicateEvaluator.filterEnumConstants(
|
||||
transitionEvents, constraint, eventTypeFqn, trigger, context);
|
||||
if (!richFiltered.isEmpty()) {
|
||||
return richFiltered;
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
return transitionEvents.size() == 1 ? transitionEvents : List.of();
|
||||
@@ -436,6 +446,17 @@ public final class MachineEnumCanonicalizer {
|
||||
String eventTypeFqn,
|
||||
List<Transition> machineTransitions,
|
||||
CodebaseContext context) {
|
||||
return narrowPolymorphicCandidates(
|
||||
current, constraint, eventTypeFqn, machineTransitions, context, null);
|
||||
}
|
||||
|
||||
private static List<String> narrowPolymorphicCandidates(
|
||||
List<String> current,
|
||||
String constraint,
|
||||
String eventTypeFqn,
|
||||
List<Transition> machineTransitions,
|
||||
CodebaseContext context,
|
||||
TriggerPoint trigger) {
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
|
||||
List<String> result = current == null ? List.of() : new ArrayList<>(current);
|
||||
|
||||
@@ -444,6 +465,23 @@ public final class MachineEnumCanonicalizer {
|
||||
result, constraint, eventTypeFqn, context);
|
||||
if (!filtered.isEmpty()) {
|
||||
result = filtered;
|
||||
} else if (trigger != null) {
|
||||
List<String> source = !transitionEvents.isEmpty() ? transitionEvents : result;
|
||||
List<String> richFiltered = RichEventPredicateEvaluator.filterEnumConstants(
|
||||
source, constraint, eventTypeFqn, trigger, context);
|
||||
if (!richFiltered.isEmpty()) {
|
||||
result = richFiltered;
|
||||
} else if (!transitionEvents.isEmpty()) {
|
||||
filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
transitionEvents, constraint, eventTypeFqn, context);
|
||||
if (!filtered.isEmpty()) {
|
||||
result = filtered;
|
||||
} else {
|
||||
result = List.of();
|
||||
}
|
||||
} else {
|
||||
result = List.of();
|
||||
}
|
||||
} else if (!transitionEvents.isEmpty()) {
|
||||
filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
transitionEvents, constraint, eventTypeFqn, context);
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.TypeResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.InfixExpression;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.QualifiedName;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Evaluates boolean predicates on rich/domain event types (e.g. {@code myEvent.isCorrect()})
|
||||
* and maps matching implementations to machine enum constants via {@code getType()}.
|
||||
*/
|
||||
public final class RichEventPredicateEvaluator {
|
||||
|
||||
private RichEventPredicateEvaluator() {
|
||||
}
|
||||
|
||||
public static List<String> filterEnumConstants(
|
||||
List<String> candidates,
|
||||
String constraint,
|
||||
String machineEnumFqn,
|
||||
TriggerPoint trigger,
|
||||
CodebaseContext context) {
|
||||
if (candidates == null || candidates.isEmpty() || constraint == null || context == null || trigger == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<EnumMemberPredicateEvaluator.PredicateCall> predicates =
|
||||
EnumMemberPredicateEvaluator.extractPredicateCalls(constraint);
|
||||
if (predicates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
EnumMemberPredicateEvaluator.PredicateCall predicate = predicates.get(0);
|
||||
String receiverTypeFqn = resolveReceiverTypeFqn(trigger, predicate.receiverName(), context);
|
||||
if (receiverTypeFqn == null) {
|
||||
return List.of();
|
||||
}
|
||||
Set<String> ownerTypes = new LinkedHashSet<>();
|
||||
ownerTypes.add(receiverTypeFqn);
|
||||
ownerTypes.addAll(context.getImplementations(receiverTypeFqn));
|
||||
|
||||
Set<String> matched = new LinkedHashSet<>();
|
||||
for (String ownerFqn : ownerTypes) {
|
||||
if (!predicateMatches(ownerFqn, predicate, context)) {
|
||||
continue;
|
||||
}
|
||||
String typeConstant = resolveDirectEnumFromPredicateMethod(ownerFqn, "getType", context);
|
||||
if (typeConstant != null && matchesCandidate(typeConstant, candidates, machineEnumFqn, context)) {
|
||||
matched.add(canonicalize(typeConstant, machineEnumFqn, context));
|
||||
}
|
||||
String directConstant = resolveDirectEnumFromPredicateMethod(ownerFqn, predicate.methodName(), context);
|
||||
if (directConstant != null && matchesCandidate(directConstant, candidates, machineEnumFqn, context)) {
|
||||
matched.add(canonicalize(directConstant, machineEnumFqn, context));
|
||||
}
|
||||
}
|
||||
List<String> filtered = new ArrayList<>();
|
||||
for (String candidate : candidates) {
|
||||
String canonical = canonicalize(candidate, machineEnumFqn, context);
|
||||
if (matched.contains(canonical)) {
|
||||
filtered.add(candidate);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static String resolveReceiverTypeFqn(TriggerPoint trigger, String receiverName, CodebaseContext context) {
|
||||
if (trigger.getClassName() == null || trigger.getMethodName() == null || receiverName == null) {
|
||||
return null;
|
||||
}
|
||||
TypeDeclaration owner = context.getTypeDeclaration(trigger.getClassName());
|
||||
if (owner == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(owner, trigger.getMethodName(), true);
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
TypeResolver typeResolver = new TypeResolver(context);
|
||||
for (Object parameter : method.parameters()) {
|
||||
if (parameter instanceof org.eclipse.jdt.core.dom.SingleVariableDeclaration variable
|
||||
&& receiverName.equals(variable.getName().getIdentifier())) {
|
||||
return typeResolver.resolveTypeToFqn(variable.getType(), owner.getRoot());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean predicateMatches(
|
||||
String ownerFqn,
|
||||
EnumMemberPredicateEvaluator.PredicateCall predicate,
|
||||
CodebaseContext context) {
|
||||
TypeDeclaration type = context.getTypeDeclaration(ownerFqn);
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(type, predicate.methodName(), true);
|
||||
if (method == null || method.getBody() == null) {
|
||||
return false;
|
||||
}
|
||||
Boolean[] result = new Boolean[1];
|
||||
method.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (result[0] == null && node.getExpression() != null) {
|
||||
result[0] = evaluateBoolean(node.getExpression());
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (result[0] == null) {
|
||||
return false;
|
||||
}
|
||||
return predicate.negated() ? !result[0] : result[0];
|
||||
}
|
||||
|
||||
private static Boolean evaluateBoolean(Expression expression) {
|
||||
if (expression instanceof org.eclipse.jdt.core.dom.BooleanLiteral literal) {
|
||||
return literal.booleanValue();
|
||||
}
|
||||
if (expression instanceof MethodInvocation invocation
|
||||
&& "isEvent".equals(invocation.getName().getIdentifier())
|
||||
&& invocation.getExpression() instanceof MethodInvocation getType
|
||||
&& "getType".equals(getType.getName().getIdentifier())) {
|
||||
return true;
|
||||
}
|
||||
if (expression instanceof InfixExpression infix && infix.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String resolveDirectEnumFromPredicateMethod(
|
||||
String ownerFqn,
|
||||
String predicateMethod,
|
||||
CodebaseContext context) {
|
||||
TypeDeclaration type = context.getTypeDeclaration(ownerFqn);
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(type, predicateMethod, true);
|
||||
if (method == null || method.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
final String[] constant = new String[1];
|
||||
method.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (constant[0] != null || node.getExpression() == null) {
|
||||
return super.visit(node);
|
||||
}
|
||||
constant[0] = extractEnumConstant(node.getExpression());
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return constant[0];
|
||||
}
|
||||
|
||||
private static String extractEnumConstant(Expression expression) {
|
||||
if (expression instanceof QualifiedName qualifiedName) {
|
||||
return qualifiedName.getFullyQualifiedName();
|
||||
}
|
||||
if (expression instanceof InfixExpression infix && infix.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||
String left = extractEnumConstant(infix.getLeftOperand());
|
||||
if (left != null) {
|
||||
return left;
|
||||
}
|
||||
return extractEnumConstant(infix.getRightOperand());
|
||||
}
|
||||
if (expression instanceof MethodInvocation invocation
|
||||
&& invocation.getExpression() instanceof MethodInvocation getType
|
||||
&& "getType".equals(getType.getName().getIdentifier())) {
|
||||
if (invocation.getExpression() instanceof MethodInvocation inner
|
||||
&& inner.getExpression() instanceof ClassInstanceCreation creation
|
||||
&& !creation.arguments().isEmpty()) {
|
||||
return extractEnumConstant((Expression) creation.arguments().get(0));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean matchesCandidate(
|
||||
String constant,
|
||||
List<String> candidates,
|
||||
String machineEnumFqn,
|
||||
CodebaseContext context) {
|
||||
if (constant == null) {
|
||||
return false;
|
||||
}
|
||||
String canonical = canonicalize(constant, machineEnumFqn, context);
|
||||
for (String candidate : candidates) {
|
||||
if (canonical.equals(canonicalize(candidate, machineEnumFqn, context))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String canonicalize(String constant, String machineEnumFqn, CodebaseContext context) {
|
||||
return MachineEnumCanonicalizer.canonicalizeLabel(constant, machineEnumFqn, context);
|
||||
}
|
||||
}
|
||||
@@ -3116,6 +3116,20 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (expr instanceof MethodInvocation mi
|
||||
&& "run".equals(mi.getName().getIdentifier())
|
||||
&& mi.arguments().isEmpty()) {
|
||||
List<String> callerFrameConstants = CallSiteArgumentResolver.resolveFunctionalRunFromCallerFrame(
|
||||
mi, path, callGraph, scopeMethod, typeResolver, pathFinder, constantExtractor, callSiteHooks());
|
||||
if (!callerFrameConstants.isEmpty()) {
|
||||
for (String constant : callerFrameConstants) {
|
||||
if (!constants.contains(constant)) {
|
||||
constants.add(constant);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
List<Expression> traced = variableTracer.traceVariableAll(expr);
|
||||
for (Expression tracedExpr : traced) {
|
||||
addConstantsFromSingleExpression(tracedExpr, constants, path, callGraph, scopeMethod);
|
||||
@@ -3155,6 +3169,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||
if (bindings.isEmpty()) {
|
||||
List<String> runnableResolved = CallSiteArgumentResolver.resolveRunnableLambdaFromPath(
|
||||
path, callGraph, typeResolver, context, pathFinder, constantExtractor, callSiteHooks());
|
||||
if (!runnableResolved.isEmpty()) {
|
||||
return runnableResolved;
|
||||
}
|
||||
if (hasMultiClassPolymorphicPath(path)
|
||||
&& polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant)) {
|
||||
List<String> callerArgumentResolved = resolvePolymorphicEventsFromCallerArgument(path);
|
||||
@@ -3593,4 +3612,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
results.add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> resolveRunnableLambdaConstantsForPath(
|
||||
List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
return CallSiteArgumentResolver.resolveRunnableLambdaFromPath(
|
||||
path, callGraph, typeResolver, context, pathFinder, constantExtractor, callSiteHooks());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.ExpressionStatement;
|
||||
import org.eclipse.jdt.core.dom.IMethodBinding;
|
||||
import org.eclipse.jdt.core.dom.LambdaExpression;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.Block;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -62,6 +71,103 @@ public final class CallSiteArgumentResolver {
|
||||
return extractConstantsFromCallSiteArgument(argValue, constantExtractor, hooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@code task.run()} is seen inside {@code scopeMethod}, walk to the caller frame and extract
|
||||
* enum/constants from the {@code Runnable} argument bound at the {@code execute(Runnable)} call site.
|
||||
*/
|
||||
public static List<String> resolveFunctionalRunFromCallerFrame(
|
||||
MethodInvocation runCall,
|
||||
List<String> path,
|
||||
Map<String, List<CallEdge>> callGraph,
|
||||
String scopeMethod,
|
||||
TypeResolver typeResolver,
|
||||
CallGraphPathFinder pathFinder,
|
||||
ConstantExtractor constantExtractor,
|
||||
ExpressionHooks hooks) {
|
||||
if (path == null || path.size() < 2 || scopeMethod == null || runCall.getExpression() == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!(runCall.getExpression() instanceof SimpleName runnableParam)) {
|
||||
return List.of();
|
||||
}
|
||||
if (!"run".equals(runCall.getName().getIdentifier()) || !runCall.arguments().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
int scopeIndex = indexOfMethod(path, scopeMethod);
|
||||
if (scopeIndex <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
String callee = path.get(scopeIndex);
|
||||
String caller = path.get(scopeIndex - 1);
|
||||
int paramIndex = typeResolver.getParameterIndex(callee, runnableParam.getIdentifier());
|
||||
if (paramIndex < 0) {
|
||||
paramIndex = findRunnableParameterIndex(callee, typeResolver);
|
||||
}
|
||||
if (paramIndex < 0) {
|
||||
return List.of();
|
||||
}
|
||||
String argValue = findCallSiteArgument(caller, callee, paramIndex, callGraph, pathFinder);
|
||||
if (argValue == null) {
|
||||
return List.of();
|
||||
}
|
||||
return extractConstantsFromCallSiteArgument(argValue, constantExtractor, hooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the call path contains {@code java.lang.Runnable.run}, resolve the {@code Runnable} lambda
|
||||
* passed from the frame two hops earlier (e.g. controller → executor.execute → Runnable.run).
|
||||
*/
|
||||
public static List<String> resolveRunnableLambdaFromPath(
|
||||
List<String> path,
|
||||
Map<String, List<CallEdge>> callGraph,
|
||||
TypeResolver typeResolver,
|
||||
CodebaseContext context,
|
||||
CallGraphPathFinder pathFinder,
|
||||
ConstantExtractor constantExtractor,
|
||||
ExpressionHooks hooks) {
|
||||
if (path == null || path.size() < 3) {
|
||||
return List.of();
|
||||
}
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
if (!isRunnableRunMethod(path.get(i))) {
|
||||
continue;
|
||||
}
|
||||
String executeMethod = path.get(i - 1);
|
||||
String callerMethod = path.get(i - 2);
|
||||
int paramIndex = findRunnableParameterIndex(executeMethod, typeResolver);
|
||||
if (paramIndex < 0) {
|
||||
paramIndex = 0;
|
||||
}
|
||||
String argValue = findCallSiteArgument(callerMethod, executeMethod, paramIndex, callGraph, pathFinder);
|
||||
List<String> constants = extractRunnableArgumentConstants(
|
||||
callerMethod, executeMethod, paramIndex, argValue, context, typeResolver, constantExtractor, hooks);
|
||||
if (!constants.isEmpty()) {
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private static boolean isRunnableRunMethod(String methodFqn) {
|
||||
return "java.lang.Runnable.run".equals(methodFqn) || "Runnable.run".equals(methodFqn);
|
||||
}
|
||||
|
||||
private static int findRunnableParameterIndex(String methodFqn, TypeResolver typeResolver) {
|
||||
if (methodFqn == null || typeResolver == null) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < 8; i++) {
|
||||
String paramType = typeResolver.getParameterType(methodFqn, i);
|
||||
if (paramType == null) {
|
||||
break;
|
||||
}
|
||||
if (FunctionalInterfaceTypes.isFunctionalInterface(paramType)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static String findCallSiteArgument(
|
||||
String caller,
|
||||
String callee,
|
||||
@@ -110,11 +216,201 @@ public final class CallSiteArgumentResolver {
|
||||
Expression resolvedExpr = hooks.parseExpression(resolved);
|
||||
List<String> constants = new ArrayList<>();
|
||||
if (resolvedExpr != null) {
|
||||
constantExtractor.extractConstantsFromExpression(resolvedExpr, constants);
|
||||
extractSideEffectConstantsFromLambda(resolvedExpr, constantExtractor, constants);
|
||||
if (constants.isEmpty()) {
|
||||
constantExtractor.extractConstantsFromExpression(resolvedExpr, constants);
|
||||
}
|
||||
}
|
||||
if (constants.isEmpty() && parsed instanceof LambdaExpression lambda) {
|
||||
extractSideEffectConstantsFromLambda(lambda, constantExtractor, constants);
|
||||
}
|
||||
if (constants.isEmpty() && FunctionalInterfaceTypes.looksLikeEnumConstant(resolved)) {
|
||||
constants.add(resolved);
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
|
||||
private static void extractSideEffectConstantsFromLambda(
|
||||
Expression expression,
|
||||
ConstantExtractor constantExtractor,
|
||||
List<String> constants) {
|
||||
if (!(expression instanceof LambdaExpression lambda)) {
|
||||
return;
|
||||
}
|
||||
ASTNode body = lambda.getBody();
|
||||
if (body instanceof Expression bodyExpr) {
|
||||
constantExtractor.extractConstantsFromExpression(bodyExpr, constants);
|
||||
return;
|
||||
}
|
||||
if (!(body instanceof Block block)) {
|
||||
return;
|
||||
}
|
||||
for (Object statement : block.statements()) {
|
||||
if (statement instanceof ExpressionStatement expressionStatement) {
|
||||
constantExtractor.extractConstantsFromExpression(expressionStatement.getExpression(), constants);
|
||||
} else if (statement instanceof org.eclipse.jdt.core.dom.ReturnStatement returnStatement
|
||||
&& returnStatement.getExpression() != null) {
|
||||
constantExtractor.extractConstantsFromExpression(returnStatement.getExpression(), constants);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> extractRunnableArgumentConstants(
|
||||
String callerMethod,
|
||||
String executeMethod,
|
||||
int paramIndex,
|
||||
String argValue,
|
||||
CodebaseContext context,
|
||||
TypeResolver typeResolver,
|
||||
ConstantExtractor constantExtractor,
|
||||
ExpressionHooks hooks) {
|
||||
List<String> constants = new ArrayList<>();
|
||||
Expression sourceArg = findCallSiteArgumentExpression(callerMethod, executeMethod, paramIndex, context);
|
||||
if (sourceArg != null) {
|
||||
extractConstantsViaDataflow(sourceArg, context, constantExtractor, constants);
|
||||
}
|
||||
if (constants.isEmpty() && argValue != null) {
|
||||
Expression parsed = hooks.parseExpression(argValue);
|
||||
if (parsed != null) {
|
||||
extractConstantsViaDataflow(parsed, context, constantExtractor, constants);
|
||||
if (constants.isEmpty() && parsed instanceof MethodInvocation sideEffect) {
|
||||
extractSideEffectConstantsFromParsedInvocation(
|
||||
sideEffect, callerMethod, constantExtractor, constants, context, typeResolver);
|
||||
}
|
||||
}
|
||||
if (constants.isEmpty()) {
|
||||
constants.addAll(extractConstantsFromCallSiteArgument(argValue, constantExtractor, hooks));
|
||||
}
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
|
||||
private static void extractSideEffectConstantsFromParsedInvocation(
|
||||
MethodInvocation invocation,
|
||||
String callerMethodFqn,
|
||||
ConstantExtractor constantExtractor,
|
||||
List<String> constants,
|
||||
CodebaseContext context,
|
||||
TypeResolver typeResolver) {
|
||||
if (context == null || typeResolver == null || callerMethodFqn == null || !callerMethodFqn.contains(".")) {
|
||||
return;
|
||||
}
|
||||
String callerClass = callerMethodFqn.substring(0, callerMethodFqn.lastIndexOf('.'));
|
||||
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
||||
if (callerType == null || !(invocation.getExpression() instanceof SimpleName receiver)) {
|
||||
return;
|
||||
}
|
||||
String receiverFqn = null;
|
||||
for (Object field : callerType.getFields()) {
|
||||
if (field instanceof org.eclipse.jdt.core.dom.FieldDeclaration fieldDeclaration) {
|
||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
||||
if (fragmentObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment fragment
|
||||
&& receiver.getIdentifier().equals(fragment.getName().getIdentifier())) {
|
||||
receiverFqn = typeResolver.resolveTypeToFqn(fieldDeclaration.getType(), callerType.getRoot());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (receiverFqn != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (receiverFqn == null) {
|
||||
return;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(
|
||||
context.getTypeDeclaration(receiverFqn),
|
||||
invocation.getName().getIdentifier(),
|
||||
true);
|
||||
if (method == null || method.getBody() == null) {
|
||||
return;
|
||||
}
|
||||
for (Object statement : method.getBody().statements()) {
|
||||
if (statement instanceof ExpressionStatement expressionStatement
|
||||
&& expressionStatement.getExpression() instanceof MethodInvocation nested) {
|
||||
for (Object arg : nested.arguments()) {
|
||||
constantExtractor.extractConstantsFromExpression((Expression) arg, constants);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractConstantsViaDataflow(
|
||||
Expression expression,
|
||||
CodebaseContext context,
|
||||
ConstantExtractor constantExtractor,
|
||||
List<String> constants) {
|
||||
Expression effective = unwrapLambdaBody(expression);
|
||||
if (effective != expression) {
|
||||
extractConstantsViaDataflow(effective, context, constantExtractor, constants);
|
||||
if (!constants.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
JdtDataFlowModel dataFlow = new JdtDataFlowModel(context);
|
||||
for (Expression def : dataFlow.getReachingDefinitions(effective)) {
|
||||
constantExtractor.extractConstantsFromExpression(def, constants);
|
||||
}
|
||||
if (constants.isEmpty()) {
|
||||
constantExtractor.extractConstantsFromExpression(effective, constants);
|
||||
}
|
||||
}
|
||||
|
||||
private static Expression unwrapLambdaBody(Expression expression) {
|
||||
if (!(expression instanceof LambdaExpression lambda)) {
|
||||
return expression;
|
||||
}
|
||||
ASTNode body = lambda.getBody();
|
||||
if (body instanceof Expression bodyExpr) {
|
||||
return bodyExpr;
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
static Expression findCallSiteArgumentExpression(
|
||||
String caller,
|
||||
String callee,
|
||||
int paramIndex,
|
||||
CodebaseContext context) {
|
||||
if (caller == null || callee == null || paramIndex < 0 || context == null
|
||||
|| !caller.contains(".") || !callee.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
String callerClass = caller.substring(0, caller.lastIndexOf('.'));
|
||||
String callerMethod = caller.substring(caller.lastIndexOf('.') + 1);
|
||||
String calleeClass = callee.substring(0, callee.lastIndexOf('.'));
|
||||
String calleeMethod = callee.substring(callee.lastIndexOf('.') + 1);
|
||||
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
||||
if (callerType == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(callerType, callerMethod, true);
|
||||
if (method == null || method.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
final Expression[] found = new Expression[1];
|
||||
method.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (found[0] != null || !calleeMethod.equals(node.getName().getIdentifier())) {
|
||||
return super.visit(node);
|
||||
}
|
||||
IMethodBinding binding = node.resolveMethodBinding();
|
||||
if (binding != null && binding.getDeclaringClass() != null) {
|
||||
String declaringFqn = binding.getDeclaringClass().getErasure().getQualifiedName();
|
||||
if (!declaringFqn.equals(calleeClass) && !declaringFqn.endsWith("." + calleeClass)) {
|
||||
String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
|
||||
if (!declaringFqn.endsWith("." + calleeSimple)) {
|
||||
return super.visit(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (paramIndex < node.arguments().size()) {
|
||||
found[0] = (Expression) node.arguments().get(paramIndex);
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return found[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ public final class FunctionalInterfaceTypes {
|
||||
if (simple.contains("<")) {
|
||||
simple = simple.substring(0, simple.indexOf('<'));
|
||||
}
|
||||
return "Supplier".equals(simple) || "Function".equals(simple) || "Callable".equals(simple);
|
||||
return "Supplier".equals(simple) || "Function".equals(simple) || "Callable".equals(simple)
|
||||
|| "Runnable".equals(simple);
|
||||
}
|
||||
|
||||
public static boolean isLambdaArgument(String argValue) {
|
||||
|
||||
@@ -253,6 +253,9 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
collectSideEffectExpressions(
|
||||
md.getBody(), results, visited, newParamBindings, target.fieldBindings, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1463,6 +1466,33 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return findMethodDeclarationInType(mb.getDeclaringClass(), mb);
|
||||
}
|
||||
|
||||
private void collectSideEffectExpressions(
|
||||
Block body,
|
||||
List<Expression> results,
|
||||
Set<ASTNode> visited,
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> fieldBindings,
|
||||
int depth) {
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
body.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ExpressionStatement node) {
|
||||
Expression statementExpr = node.getExpression();
|
||||
if (statementExpr instanceof MethodInvocation nested) {
|
||||
for (Object argObj : nested.arguments()) {
|
||||
results.addAll(getReachingDefinitions(
|
||||
(Expression) argObj, visited, paramBindings, fieldBindings, depth + 1));
|
||||
}
|
||||
}
|
||||
results.addAll(getReachingDefinitions(
|
||||
statementExpr, visited, paramBindings, fieldBindings, depth + 1));
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<ReturnStatement> findReturnStatements(ASTNode node) {
|
||||
List<ReturnStatement> returns = new ArrayList<>();
|
||||
node.accept(new ASTVisitor() {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RichEventIsCorrectPredicateTest {
|
||||
|
||||
@Test
|
||||
void shouldFilterEnumConstantsByRichEventIsCorrectPredicate(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanProject(tempDir);
|
||||
|
||||
List<String> all = List.of(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP",
|
||||
"com.example.order.OrderCommand.META");
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.service.AbstractOrderService")
|
||||
.methodName("dispatch")
|
||||
.build();
|
||||
|
||||
List<String> filtered = RichEventPredicateEvaluator.filterEnumConstants(
|
||||
all, "myEvent.isCorrect()", "com.example.order.OrderCommand", trigger, context);
|
||||
|
||||
assertThat(filtered).containsExactly(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInferPolymorphicEventsForRichEventConstraint(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanProject(tempDir);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.service.AbstractOrderService")
|
||||
.methodName("dispatch")
|
||||
.event("myEvent.getType()")
|
||||
.constraint("myEvent.isCorrect()")
|
||||
.external(true)
|
||||
.ambiguous(true)
|
||||
.build();
|
||||
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
|
||||
List<Transition> transitions = List.of(
|
||||
transition("OrderCommand.PAY", "NEW", "PAID"),
|
||||
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
|
||||
|
||||
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||
trigger, types, context, transitions, true);
|
||||
|
||||
assertThat(enriched.getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP");
|
||||
assertThat(enriched.isAmbiguous()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLinkTransitionsWhenConstraintUsesRichEventPredicate(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanProject(tempDir);
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.service.AbstractOrderService")
|
||||
.methodName("dispatch")
|
||||
.event("myEvent.getType()")
|
||||
.polymorphicEvents(List.of(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP",
|
||||
"com.example.order.OrderCommand.META"))
|
||||
.constraint("myEvent.isCorrect()")
|
||||
.ambiguous(true)
|
||||
.external(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
|
||||
Transition ship = transition("OrderCommand.SHIP", "PAID", "SHIPPED");
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.eventTypeFqn("com.example.order.OrderCommand")
|
||||
.transitions(List.of(pay, ship))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
new TransitionLinkerEnricher().enrich(result, context, null);
|
||||
|
||||
TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(tp.getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP");
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
|
||||
}
|
||||
|
||||
private static CodebaseContext scanProject(Path tempDir) throws Exception {
|
||||
Path javaRoot = tempDir.resolve("src/main/java/com/example");
|
||||
Files.createDirectories(javaRoot.resolve("order"));
|
||||
Files.createDirectories(javaRoot.resolve("domain"));
|
||||
Files.createDirectories(javaRoot.resolve("service"));
|
||||
Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
|
||||
Files.writeString(javaRoot.resolve("order/OrderCommand.java"), """
|
||||
package com.example.order;
|
||||
public enum OrderCommand { PAY, SHIP, META }
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("domain/DomainEvent.java"), """
|
||||
package com.example.domain;
|
||||
import com.example.order.OrderCommand;
|
||||
public interface DomainEvent {
|
||||
OrderCommand getType();
|
||||
boolean isCorrect();
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("domain/PayEvent.java"), """
|
||||
package com.example.domain;
|
||||
import com.example.order.OrderCommand;
|
||||
public class PayEvent implements DomainEvent {
|
||||
@Override
|
||||
public OrderCommand getType() { return OrderCommand.PAY; }
|
||||
@Override
|
||||
public boolean isCorrect() { return true; }
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("domain/ShipEvent.java"), """
|
||||
package com.example.domain;
|
||||
import com.example.order.OrderCommand;
|
||||
public class ShipEvent implements DomainEvent {
|
||||
@Override
|
||||
public OrderCommand getType() { return OrderCommand.SHIP; }
|
||||
@Override
|
||||
public boolean isCorrect() { return true; }
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("domain/MetaEvent.java"), """
|
||||
package com.example.domain;
|
||||
import com.example.order.OrderCommand;
|
||||
public class MetaEvent implements DomainEvent {
|
||||
@Override
|
||||
public OrderCommand getType() { return OrderCommand.META; }
|
||||
@Override
|
||||
public boolean isCorrect() { return false; }
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("service/AbstractOrderService.java"), """
|
||||
package com.example.service;
|
||||
import com.example.domain.DomainEvent;
|
||||
import com.example.order.OrderCommand;
|
||||
public abstract class AbstractOrderService {
|
||||
protected void dispatch(DomainEvent myEvent) {
|
||||
if (myEvent.isCorrect()) {
|
||||
fire(myEvent.getType());
|
||||
}
|
||||
}
|
||||
protected abstract void fire(OrderCommand event);
|
||||
}
|
||||
""");
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
|
||||
context.scan(tempDir);
|
||||
return context;
|
||||
}
|
||||
|
||||
private static Transition transition(String event, String source, String target) {
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of(event, event));
|
||||
transition.setSourceStates(List.of(State.of(source, source)));
|
||||
transition.setTargetStates(List.of(State.of(target, target)));
|
||||
return transition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RunnableLambdaDispatchTest {
|
||||
|
||||
@Test
|
||||
void shouldResolveEventThroughRunnableExecuteLambda() throws IOException {
|
||||
String controllerSource = """
|
||||
package com.example.web;
|
||||
|
||||
import com.example.service.AbstractOrderService;
|
||||
import com.example.executor.TaskExecutor;
|
||||
|
||||
public class OrderController {
|
||||
private final TaskExecutor executor;
|
||||
private final AbstractOrderService orderService;
|
||||
|
||||
public OrderController(TaskExecutor executor, AbstractOrderService orderService) {
|
||||
this.executor = executor;
|
||||
this.orderService = orderService;
|
||||
}
|
||||
|
||||
public void process() {
|
||||
executor.execute(() -> orderService.fireEvent());
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String executorSource = """
|
||||
package com.example.executor;
|
||||
|
||||
public class TaskExecutor {
|
||||
public void execute(Runnable task) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String serviceSource = """
|
||||
package com.example.service;
|
||||
|
||||
import com.example.domain.OrderCommand;
|
||||
|
||||
public abstract class AbstractOrderService {
|
||||
protected StateMachine machine;
|
||||
|
||||
public void fireEvent() {
|
||||
sendEvent(OrderCommand.PAY);
|
||||
}
|
||||
|
||||
protected void sendEvent(OrderCommand event) {
|
||||
machine.fire(event);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
public void fire(OrderCommand event) {}
|
||||
}
|
||||
""";
|
||||
|
||||
String enumSource = """
|
||||
package com.example.domain;
|
||||
|
||||
public enum OrderCommand { PAY, SHIP, META }
|
||||
""";
|
||||
|
||||
Path tempDir = Files.createTempDirectory("callgraph_runnable_lambda");
|
||||
Files.writeString(tempDir.resolve("OrderController.java"), controllerSource);
|
||||
Files.writeString(tempDir.resolve("TaskExecutor.java"), executorSource);
|
||||
Files.writeString(tempDir.resolve("AbstractOrderService.java"), serviceSource);
|
||||
Files.writeString(tempDir.resolve("OrderCommand.java"), enumSource);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("process")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.service.AbstractOrderService")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderCommand.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveRunnableLambdaFromExecuteCallSite() throws IOException {
|
||||
String controllerSource = """
|
||||
package com.example.web;
|
||||
|
||||
import com.example.service.AbstractOrderService;
|
||||
import com.example.executor.TaskExecutor;
|
||||
|
||||
public class OrderController {
|
||||
private final TaskExecutor executor;
|
||||
private final AbstractOrderService orderService;
|
||||
|
||||
public OrderController(TaskExecutor executor, AbstractOrderService orderService) {
|
||||
this.executor = executor;
|
||||
this.orderService = orderService;
|
||||
}
|
||||
|
||||
public void process() {
|
||||
executor.execute(() -> orderService.fireEvent());
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String executorSource = """
|
||||
package com.example.executor;
|
||||
|
||||
public class TaskExecutor {
|
||||
public void execute(Runnable task) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String serviceSource = """
|
||||
package com.example.service;
|
||||
|
||||
import com.example.domain.OrderCommand;
|
||||
|
||||
public abstract class AbstractOrderService {
|
||||
public void fireEvent() {
|
||||
sendEvent(OrderCommand.PAY);
|
||||
}
|
||||
|
||||
protected void sendEvent(OrderCommand event) {}
|
||||
}
|
||||
""";
|
||||
|
||||
String enumSource = """
|
||||
package com.example.domain;
|
||||
|
||||
public enum OrderCommand { PAY, SHIP, META }
|
||||
""";
|
||||
|
||||
Path tempDir = Files.createTempDirectory("callgraph_runnable_resolver");
|
||||
Files.writeString(tempDir.resolve("OrderController.java"), controllerSource);
|
||||
Files.writeString(tempDir.resolve("TaskExecutor.java"), executorSource);
|
||||
Files.writeString(tempDir.resolve("AbstractOrderService.java"), serviceSource);
|
||||
Files.writeString(tempDir.resolve("OrderCommand.java"), enumSource);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
var graph = engine.buildCallGraph();
|
||||
|
||||
assertThat(graph.get("com.example.executor.TaskExecutor.execute"))
|
||||
.extracting(click.kamil.springstatemachineexporter.analysis.model.CallEdge::getTargetMethod)
|
||||
.anyMatch(target -> target.endsWith("Runnable.run"));
|
||||
|
||||
List<String> path = List.of(
|
||||
"com.example.web.OrderController.process",
|
||||
"com.example.executor.TaskExecutor.execute",
|
||||
graph.get("com.example.executor.TaskExecutor.execute").get(0).getTargetMethod(),
|
||||
"com.example.service.AbstractOrderService.fireEvent");
|
||||
|
||||
List<String> resolved = engine.resolveRunnableLambdaConstantsForPath(path, graph);
|
||||
assertThat(resolved).containsExactly("OrderCommand.PAY");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user