update tranistions 2

This commit is contained in:
2026-06-28 10:13:11 +02:00
parent 8d3fb0d90c
commit 96fc975170
14 changed files with 362 additions and 12 deletions

View File

@@ -29,6 +29,13 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
}
String classPe = pe.substring(0, pe.lastIndexOf('.'));
String classSm = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
if (classPe.contains(".") && classSm.contains(".")) {
String p1 = classPe.substring(0, classPe.lastIndexOf('.'));
String p2 = classSm.substring(0, classSm.lastIndexOf('.'));
if (!p1.equals(p2) && !p1.startsWith(p2 + ".") && !p2.startsWith(p1 + ".")) {
continue;
}
}
String simpleClassPe = classPe.contains(".") ? classPe.substring(classPe.lastIndexOf('.') + 1) : classPe;
String simpleClassSm = classSm.contains(".") ? classSm.substring(classSm.lastIndexOf('.') + 1) : classSm;
if (!simpleClassPe.equals(simpleClassSm)) {
@@ -59,6 +66,13 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
}
String classTrig = rawTriggerEvent.substring(0, rawTriggerEvent.lastIndexOf('.'));
String classSm = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
if (classTrig.contains(".") && classSm.contains(".")) {
String p1 = classTrig.substring(0, classTrig.lastIndexOf('.'));
String p2 = classSm.substring(0, classSm.lastIndexOf('.'));
if (!p1.equals(p2) && !p1.startsWith(p2 + ".") && !p2.startsWith(p1 + ".")) {
return false;
}
}
String simpleClassTrig = classTrig.contains(".") ? classTrig.substring(classTrig.lastIndexOf('.') + 1) : classTrig;
String simpleClassSm = classSm.contains(".") ? classSm.substring(classSm.lastIndexOf('.') + 1) : classSm;
if (!simpleClassTrig.equals(simpleClassSm)) {

View File

@@ -20,7 +20,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
String machineEventFqn = machineTypes[1];
if (triggerEventFqn != null && machineEventFqn != null) {
if (triggerEventFqn.equals(machineEventFqn)) {
if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) {
return true; // Match!
}
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
@@ -28,7 +28,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
}
}
if (triggerStateFqn != null && machineStateFqn != null) {
if (triggerStateFqn.equals(machineStateFqn)) {
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
return true; // Match!
}
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
@@ -314,6 +314,15 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
return new String[]{null, null};
}
private String eraseGenerics(String type) {
if (type == null) return null;
int idx = type.indexOf('<');
if (idx != -1) {
return type.substring(0, idx);
}
return type;
}
private String resolveTypeToFqn(org.eclipse.jdt.core.dom.Type type, org.eclipse.jdt.core.dom.CompilationUnit cu, CodebaseContext context) {
if (type == null) return null;
String simpleName;
@@ -325,6 +334,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
simpleName = type.toString();
}
if (simpleName.contains("<")) {
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
}
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
@@ -349,6 +362,8 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
private boolean isTypeMismatched(String type1, String type2) {
if (type1 == null || type2 == null) return false;
type1 = eraseGenerics(type1);
type2 = eraseGenerics(type2);
if (type1.equals("java.lang.String") || type1.equals("String") || type1.equals("java.lang.Object") || type1.equals("Object")) return false;
if (type2.equals("java.lang.String") || type2.equals("String") || type2.equals("java.lang.Object") || type2.equals("Object")) return false;
return !type1.equals(type2);

View File

@@ -170,6 +170,9 @@ public class ConstantResolver {
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
if (!visited.add(methodFqn)) {
if (log.isDebugEnabled()) {
log.debug("Cycle detected in method evaluation for: {} (visited={})", methodFqn, visited);
}
return null; // Cycle detected in method evaluation
}
@@ -213,6 +216,9 @@ public class ConstantResolver {
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
if (!visited.add(methodFqn)) {
if (log.isDebugEnabled()) {
log.debug("Cycle detected in method body evaluation for: {} (visited={})", methodFqn, visited);
}
return null; // Cycle detected in method evaluation
}

View File

@@ -959,7 +959,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (depth > 5) return mi;
if (mi.getExpression() != null) {
String exprStr = mi.getExpression().toString();
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
return mi;
}
} else {

View File

@@ -15,7 +15,12 @@ public class CallGraphPathFinder {
public List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
if (start.equals(target)) return new ArrayList<>(List.of(start));
if (!visited.add(start)) return null; // Path-scoped cycle detection
if (!visited.add(start)) {
if (log.isDebugEnabled()) {
log.debug("Cycle detected in call graph search: {} is already in visited path {}", start, visited);
}
return null; // Path-scoped cycle detection
}
List<CallEdge> neighbors = graph.get(start);
if (neighbors != null) {

View File

@@ -87,7 +87,8 @@ public class ConstantExtractor {
}
return;
}
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry")) {
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry") ||
methodName.equals("just") || methodName.equals("withPayload") || methodName.equals("success")) {
for (Object argObj : mi.arguments()) {
extractConstantsFromExpression((Expression) argObj, constants);
}

View File

@@ -36,6 +36,16 @@ public class GenericEventDetector {
if ("sendEvent".equals(methodName) || "sendEvents".equals(methodName) ||
"sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) {
processSendEvent(node, cu, triggers);
} else {
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String refMethod = emr.getName().getIdentifier();
if ("sendEvent".equals(refMethod) || "sendEvents".equals(refMethod) ||
"sendEventCollect".equals(refMethod) || "sendEventMono".equals(refMethod)) {
processSendEventMethodReference(node, emr, cu, triggers);
}
}
}
}
processHints(node, cu, triggers);
@@ -46,6 +56,81 @@ public class GenericEventDetector {
return triggers;
}
private void processSendEventMethodReference(MethodInvocation node, ExpressionMethodReference emr, CompilationUnit cu, List<TriggerPoint> triggers) {
Expression receiver = node.getExpression();
String eventValue = extractEventFromReceiver(receiver);
if (eventValue == null) {
eventValue = receiver != null ? receiver.toString() : null;
}
MethodDeclaration method = findEnclosingMethod(node);
TypeDeclaration type = findEnclosingType(node);
if (type == null) return;
String sourceState = extractSourceState(node);
String[] smTypes = resolveStateMachineTypeArgumentsForExpression(emr.getExpression(), node);
if (eventValue != null) {
triggers.add(TriggerPoint.builder()
.event(eventValue)
.sourceState(sourceState)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.stateTypeFqn(smTypes[0])
.eventTypeFqn(smTypes[1])
.build());
}
}
private String extractEventFromReceiver(Expression receiver) {
if (receiver == null) return null;
if (receiver instanceof MethodInvocation mi) {
String mName = mi.getName().getIdentifier();
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
Expression arg = (Expression) mi.arguments().get(0);
String resolved = constantResolver.resolve(arg, context);
return resolved != null ? resolved : arg.toString();
}
if (mi.getExpression() != null) {
return extractEventFromReceiver(mi.getExpression());
}
}
if (receiver instanceof SimpleName sn) {
return sn.getIdentifier();
}
return null;
}
private String[] resolveStateMachineTypeArgumentsForExpression(Expression receiver, MethodInvocation node) {
if (receiver == null) return new String[]{null, null};
ITypeBinding binding = receiver.resolveTypeBinding();
if (binding != null) {
ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
if (smBinding != null) {
ITypeBinding[] typeArgs = smBinding.getTypeArguments();
if (typeArgs.length >= 2) {
return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()};
}
}
}
Type declType = findReceiverTypeManually(receiver, node);
if (declType instanceof ParameterizedType pt) {
List<?> typeArgs = pt.typeArguments();
if (typeArgs.size() >= 2) {
CompilationUnit compilationUnit = (CompilationUnit) node.getRoot();
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit);
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit);
return new String[]{stateType, eventType};
}
}
return new String[]{null, null};
}
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
if (builtTriggers != null) {

View File

@@ -546,7 +546,7 @@ public class VariableTracer {
if (depth > 5) return mi;
if (mi.getExpression() != null) {
String exprStr = mi.getExpression().toString();
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
return mi;
}
} else {

View File

@@ -361,6 +361,115 @@ public class AstTransitionParser {
return mr.toString();
}
// 5. Bean Reference (SimpleName, FieldAccess, etc.)
String resolvedBeanBody = resolveBeanClassBody(expr, context);
if (resolvedBeanBody != null) {
return resolvedBeanBody;
}
return null;
}
private static String resolveBeanClassBody(Expression expr, CodebaseContext context) {
String typeFqn = null;
ITypeBinding binding = expr.resolveTypeBinding();
if (binding != null) {
typeFqn = binding.getQualifiedName();
} else if (expr instanceof SimpleName sn) {
// Check enclosing method first for local variables
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
if (enclosingMethod != null) {
final String[] foundType = {null};
final Expression[] foundInitializer = {null};
enclosingMethod.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement vds) {
for (Object fragObj : vds.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
foundType[0] = vds.getType().toString();
foundInitializer[0] = fragment.getInitializer();
}
}
}
return super.visit(vds);
}
});
if (foundInitializer[0] != null) {
ASTNode root = expr.getRoot();
if (root instanceof CompilationUnit cu) {
String initLogic = extractInternalLogic(foundInitializer[0], cu, context);
if (initLogic != null) return initLogic;
}
}
if (foundType[0] != null) {
typeFqn = foundType[0];
}
}
if (typeFqn == null) {
TypeDeclaration enclosingClass = findEnclosingType(expr);
if (enclosingClass != null) {
for (FieldDeclaration fd : enclosingClass.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
if (fragment.getInitializer() != null) {
ASTNode root = expr.getRoot();
if (root instanceof CompilationUnit cu) {
String initLogic = extractInternalLogic(fragment.getInitializer(), cu, context);
if (initLogic != null) return initLogic;
}
}
typeFqn = fd.getType().toString();
break;
}
}
}
}
}
}
}
if (typeFqn != null) {
if (typeFqn.contains("<")) {
typeFqn = typeFqn.substring(0, typeFqn.indexOf('<'));
}
TypeDeclaration beanTd = context.getTypeDeclaration(typeFqn);
if (beanTd == null) {
ASTNode root = expr.getRoot();
if (root instanceof CompilationUnit cu) {
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
if (!imp.isStatic() && !imp.isOnDemand()) {
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + typeFqn)) {
beanTd = context.getTypeDeclaration(impName);
break;
}
}
}
}
}
if (beanTd != null) {
for (MethodDeclaration md : beanTd.getMethods()) {
String name = md.getName().getIdentifier();
if ("evaluate".equals(name) || "execute".equals(name)) {
return md.toString();
}
}
return beanTd.toString();
}
}
return null;
}
private static MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof MethodDeclaration md) return md;
current = current.getParent();
}
return null;
}

View File

@@ -356,11 +356,20 @@ public class CodebaseContext {
public List<String> getEnumValues(String fqnOrSimpleName) {
if (fqnOrSimpleName == null) return null;
List<String> values = enumValues.get(fqnOrSimpleName);
String cleanName = fqnOrSimpleName;
if (cleanName.contains("<")) {
cleanName = cleanName.substring(0, cleanName.indexOf('<'));
}
if (cleanName.contains("[")) {
cleanName = cleanName.substring(0, cleanName.indexOf('['));
}
cleanName = cleanName.trim();
List<String> values = enumValues.get(cleanName);
if (values != null) return values;
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
String fqn = simpleNameToFqn.get(cleanName);
if (fqn != null) return enumValues.get(fqn);
// Also try stripping array or generic types if any (e.g. MyEnum[])
return null;
}

View File

@@ -136,6 +136,12 @@ public class JdtDataFlowModel implements DataFlowModel {
// 4. Handle Method Invocations (Static Factory methods / Transforming Getters)
if (expr instanceof MethodInvocation mi) {
String mName = mi.getName().getIdentifier();
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
List<Expression> resolved = getReachingDefinitions((Expression) mi.arguments().get(0), visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
}
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth);

View File

@@ -112,7 +112,7 @@ class HeuristicEventMatchingEngineTest {
Event smEvent = Event.of("CREATE", "com.example.other.OrderEvent.CREATE");
TriggerPoint triggerPoint = TriggerPoint.builder().event("com.example.some.OrderEvent.CREATE").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
@@ -123,6 +123,6 @@ class HeuristicEventMatchingEngineTest {
.polymorphicEvents(List.of("com.example.some.OrderEvent.CREATE"))
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
}

View File

@@ -1003,4 +1003,37 @@ class HeuristicCallGraphEngineCoreTest {
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("RECORD_EVENT");
}
@Test
void shouldResolveEventFromReactiveMethodReference(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private StateMachine stateMachine;
public void handleReactive() {
Mono.just("REACTIVE_EVENT").flatMap(stateMachine::sendEvent);
}
}
class StateMachine {
public void sendEvent(String event) {}
}
class Mono {
public static Mono just(Object obj) { return new Mono(); }
public Mono flatMap(Object mapper) { return this; }
}
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleReactive").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("REACTIVE_EVENT");
}
}

View File

@@ -326,4 +326,71 @@ class AstTransitionParserTest {
}
return null;
}
@Test
void shouldExtractBeanClassBodyForExternalGuard(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tempDir) throws java.io.IOException {
String guardSource = """
package com.example;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.StateContext;
public class CustomGuard implements Guard<String, String> {
@Override
public boolean evaluate(StateContext<String, String> context) {
return "OK".equals(context.getMessage().getPayload());
}
}
""";
String configSource = """
package com.example;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public class TestClass {
private CustomGuard paymentGuard;
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY")
.guard(paymentGuard);
}
}
""";
java.nio.file.Files.writeString(tempDir.resolve("CustomGuard.java"), guardSource);
java.nio.file.Files.writeString(tempDir.resolve("TestClass.java"), configSource);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.example.TestClass");
MethodDeclaration method = context.findMethodDeclaration(td, "configure", true);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
assertThat(transitions.getFirst().getGuard().internalLogic())
.contains("return \"OK\".equals(context.getMessage().getPayload());")
.contains("public boolean evaluate");
}
@Test
void shouldExtractLocalVariableLambdaGuard() {
String source = """
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
Guard<String, String> myLocalGuard = context -> "LOCAL_OK".equals(context.getMessage());
transitions.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY")
.guard(myLocalGuard);
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
assertThat(transitions.getFirst().getGuard().internalLogic())
.contains("context -> \"LOCAL_OK\".equals(context.getMessage())");
}
}