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 polymorphicEvents;
|
||||||
}
|
}
|
||||||
return narrowPolymorphicCandidates(
|
return narrowPolymorphicCandidates(
|
||||||
polymorphicEvents, constraint, eventTypeFqn, machineTransitions, context);
|
polymorphicEvents, constraint, eventTypeFqn, machineTransitions, context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,7 +303,8 @@ public final class MachineEnumCanonicalizer {
|
|||||||
expanded.getConstraint(),
|
expanded.getConstraint(),
|
||||||
machineTypes.eventTypeFqn(),
|
machineTypes.eventTypeFqn(),
|
||||||
machineTransitions,
|
machineTransitions,
|
||||||
context);
|
context,
|
||||||
|
expanded);
|
||||||
if (!narrowed.equals(expanded.getPolymorphicEvents())) {
|
if (!narrowed.equals(expanded.getPolymorphicEvents())) {
|
||||||
return expanded.toBuilder()
|
return expanded.toBuilder()
|
||||||
.polymorphicEvents(narrowed)
|
.polymorphicEvents(narrowed)
|
||||||
@@ -317,7 +318,8 @@ public final class MachineEnumCanonicalizer {
|
|||||||
expanded.getConstraint(),
|
expanded.getConstraint(),
|
||||||
machineTypes.eventTypeFqn(),
|
machineTypes.eventTypeFqn(),
|
||||||
machineTransitions,
|
machineTransitions,
|
||||||
context);
|
context,
|
||||||
|
expanded);
|
||||||
if (!machineEvents.isEmpty()) {
|
if (!machineEvents.isEmpty()) {
|
||||||
return expanded.toBuilder()
|
return expanded.toBuilder()
|
||||||
.polymorphicEvents(machineEvents)
|
.polymorphicEvents(machineEvents)
|
||||||
@@ -414,7 +416,8 @@ public final class MachineEnumCanonicalizer {
|
|||||||
String constraint,
|
String constraint,
|
||||||
String eventTypeFqn,
|
String eventTypeFqn,
|
||||||
List<Transition> machineTransitions,
|
List<Transition> machineTransitions,
|
||||||
CodebaseContext context) {
|
CodebaseContext context,
|
||||||
|
TriggerPoint trigger) {
|
||||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
|
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
|
||||||
if (transitionEvents.isEmpty()) {
|
if (transitionEvents.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
@@ -425,6 +428,13 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (!filtered.isEmpty()) {
|
if (!filtered.isEmpty()) {
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
if (trigger != null) {
|
||||||
|
List<String> richFiltered = RichEventPredicateEvaluator.filterEnumConstants(
|
||||||
|
transitionEvents, constraint, eventTypeFqn, trigger, context);
|
||||||
|
if (!richFiltered.isEmpty()) {
|
||||||
|
return richFiltered;
|
||||||
|
}
|
||||||
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return transitionEvents.size() == 1 ? transitionEvents : List.of();
|
return transitionEvents.size() == 1 ? transitionEvents : List.of();
|
||||||
@@ -436,6 +446,17 @@ public final class MachineEnumCanonicalizer {
|
|||||||
String eventTypeFqn,
|
String eventTypeFqn,
|
||||||
List<Transition> machineTransitions,
|
List<Transition> machineTransitions,
|
||||||
CodebaseContext context) {
|
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> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
|
||||||
List<String> result = current == null ? List.of() : new ArrayList<>(current);
|
List<String> result = current == null ? List.of() : new ArrayList<>(current);
|
||||||
|
|
||||||
@@ -444,6 +465,23 @@ public final class MachineEnumCanonicalizer {
|
|||||||
result, constraint, eventTypeFqn, context);
|
result, constraint, eventTypeFqn, context);
|
||||||
if (!filtered.isEmpty()) {
|
if (!filtered.isEmpty()) {
|
||||||
result = filtered;
|
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()) {
|
} else if (!transitionEvents.isEmpty()) {
|
||||||
filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||||
transitionEvents, constraint, eventTypeFqn, context);
|
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;
|
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);
|
List<Expression> traced = variableTracer.traceVariableAll(expr);
|
||||||
for (Expression tracedExpr : traced) {
|
for (Expression tracedExpr : traced) {
|
||||||
addConstantsFromSingleExpression(tracedExpr, constants, path, callGraph, scopeMethod);
|
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);
|
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||||
if (bindings.isEmpty()) {
|
if (bindings.isEmpty()) {
|
||||||
|
List<String> runnableResolved = CallSiteArgumentResolver.resolveRunnableLambdaFromPath(
|
||||||
|
path, callGraph, typeResolver, context, pathFinder, constantExtractor, callSiteHooks());
|
||||||
|
if (!runnableResolved.isEmpty()) {
|
||||||
|
return runnableResolved;
|
||||||
|
}
|
||||||
if (hasMultiClassPolymorphicPath(path)
|
if (hasMultiClassPolymorphicPath(path)
|
||||||
&& polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant)) {
|
&& polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant)) {
|
||||||
List<String> callerArgumentResolved = resolvePolymorphicEventsFromCallerArgument(path);
|
List<String> callerArgumentResolved = resolvePolymorphicEventsFromCallerArgument(path);
|
||||||
@@ -3593,4 +3612,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
results.add(parsed);
|
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;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
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.ASTNode;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
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.MethodInvocation;
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -62,6 +71,103 @@ public final class CallSiteArgumentResolver {
|
|||||||
return extractConstantsFromCallSiteArgument(argValue, constantExtractor, hooks);
|
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(
|
public static String findCallSiteArgument(
|
||||||
String caller,
|
String caller,
|
||||||
String callee,
|
String callee,
|
||||||
@@ -110,11 +216,201 @@ public final class CallSiteArgumentResolver {
|
|||||||
Expression resolvedExpr = hooks.parseExpression(resolved);
|
Expression resolvedExpr = hooks.parseExpression(resolved);
|
||||||
List<String> constants = new ArrayList<>();
|
List<String> constants = new ArrayList<>();
|
||||||
if (resolvedExpr != null) {
|
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)) {
|
if (constants.isEmpty() && FunctionalInterfaceTypes.looksLikeEnumConstant(resolved)) {
|
||||||
constants.add(resolved);
|
constants.add(resolved);
|
||||||
}
|
}
|
||||||
return constants;
|
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("<")) {
|
if (simple.contains("<")) {
|
||||||
simple = simple.substring(0, simple.indexOf('<'));
|
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) {
|
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);
|
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) {
|
private List<ReturnStatement> findReturnStatements(ASTNode node) {
|
||||||
List<ReturnStatement> returns = new ArrayList<>();
|
List<ReturnStatement> returns = new ArrayList<>();
|
||||||
node.accept(new ASTVisitor() {
|
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