event maching heuristic update

This commit is contained in:
2026-06-21 12:19:15 +02:00
parent e00f4dca81
commit 968601eefc
28 changed files with 1317 additions and 77 deletions

View File

@@ -26,7 +26,10 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
if (pe.contains(".")) {
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
}
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
String simplifiedPe = simplify(simplePe);
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
hasPolyMatch = true;
break;
}

View File

@@ -0,0 +1,22 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.List;
public interface PolymorphicEventResolver {
/**
* Attempts to resolve a dynamically passed event variable into a set of
* concrete polymorphic event names.
*
* @param triggerPoint The original trigger point to resolve.
* @param resolvedValue The current String expression (e.g., "event.getType()" or "event")
* @param path The call path trace leading up to this variable.
* @param context The codebase context for deep type resolution.
* @return The updated TriggerPoint with polymorphic events set, or the original if unable to resolve.
*/
TriggerPoint resolvePolymorphicEvents(TriggerPoint triggerPoint, String resolvedValue, List<String> path, CodebaseContext context);
}

View File

@@ -325,6 +325,10 @@ public class GenericEventDetector {
}
}
if (expr instanceof ClassInstanceCreation cic) {
return cic.getType().toString();
}
if (!(expr instanceof MethodInvocation mi)) return null;
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()

View File

@@ -142,43 +142,85 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
}
List<String> polymorphicEvents = new ArrayList<>();
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
int lastDot = resolvedValue.lastIndexOf('.');
int firstDot = resolvedValue.indexOf('.');
int openParen = resolvedValue.indexOf('(', lastDot);
if (lastDot > 0 && openParen > lastDot) {
String varName = resolvedValue.substring(0, firstDot);
if (varName.contains("(")) {
varName = null;
// Parse resolvedValue using JDT to robustly handle complex expressions
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.getJLSLatest());
exprParser.setSource(resolvedValue.toCharArray());
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
String varName = null;
String methodName = null;
String declaredType = null;
String sourceMethod = null;
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
methodName = mi.getName().getIdentifier();
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
declaredType = ce.getType().toString();
sourceMethod = "inline-cast";
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
} else {
// Fallback for complex chained expressions
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
if (!exprStr.contains("(")) {
varName = exprStr;
}
String methodName = resolvedValue.substring(lastDot + 1, openParen);
String declaredType = null;
String sourceMethod = null;
if (varName != null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
}
}
} else {
String baseExpr = resolvedValue.substring(0, lastDot);
if (baseExpr.contains("new ")) {
int newIdx = baseExpr.indexOf("new ");
int openParenBase = baseExpr.indexOf('(', newIdx);
if (openParenBase > newIdx) {
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
if (declaredType.contains("<")) {
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
}
sourceMethod = "inline-instantiation";
}
}
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType); // Track the payload type as the event
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ConditionalExpression cond) {
if (cond.getThenExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicThen) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType()));
}
if (cond.getElseExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicElse) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType()));
}
sourceMethod = "inline-ternary";
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
methodName = "VariableReference"; // We just want to trigger deep trace
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
}
if (methodName != null) {
if (varName != null && declaredType == null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
}
}
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
if (declaredType == null && varName.matches("^[A-Z].*")) {
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
if (staticTd != null) {
declaredType = context.getFqn(staticTd);
sourceMethod = "static-call";
}
}
}
if (declaredType != null) {
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
@@ -215,9 +257,6 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
}
}
}
}
// LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
// for string literals or enum-like constants.
boolean hasValidConstant = false;
@@ -259,7 +298,16 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
return !val.equals(val.toUpperCase()) || val.length() <= 2;
});
}
List<String> newPolyEvents = new ArrayList<>();
for (String pe : polymorphicEvents) {
List<String> resolved = resolveClassConstantReturns(pe, context, null);
if (resolved != null && !resolved.isEmpty()) {
newPolyEvents.addAll(resolved);
} else {
newPolyEvents.add(pe);
}
}
polymorphicEvents = newPolyEvents;
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
@@ -384,6 +432,10 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
} else {
constants.add(val);
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) {
extractConstantsFromExpression(ce, constants);
} else if (retExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
constants.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
@@ -405,6 +457,43 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
return constants;
}
private List<String> resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) {
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
if (td == null) td = context.getTypeDeclaration(className);
if (td == null) return null;
final List<String> resolvedConstants = new ArrayList<>();
for (MethodDeclaration md : td.getMethods()) {
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
extractConstantsFromExpression(node.getExpression(), resolvedConstants);
return super.visit(node);
}
});
}
if (!resolvedConstants.isEmpty()) return resolvedConstants;
}
return null;
}
private void extractConstantsFromExpression(org.eclipse.jdt.core.dom.Expression expr, List<String> constants) {
System.out.println("EXTRACT CONST: " + (expr != null ? expr.getClass().getSimpleName() + " " + expr.toString() : "null"));
if (expr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
constants.add(sl.getLiteralValue());
} else if (expr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) {
extractConstantsFromExpression(ce.getThenExpression(), constants);
extractConstantsFromExpression(ce.getElseExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe) {
extractConstantsFromExpression(pe.getExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) {
extractConstantsFromExpression(ce.getExpression(), constants);
}
}
private String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));

View File

@@ -146,43 +146,85 @@ public class JdtCallGraphEngine implements CallGraphEngine {
}
List<String> polymorphicEvents = new ArrayList<>();
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
int lastDot = resolvedValue.lastIndexOf('.');
int firstDot = resolvedValue.indexOf('.');
int openParen = resolvedValue.indexOf('(', lastDot);
if (lastDot > 0 && openParen > lastDot) {
String varName = resolvedValue.substring(0, firstDot);
if (varName.contains("(")) {
varName = null;
// Parse resolvedValue using JDT to robustly handle complex expressions
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.getJLSLatest());
exprParser.setSource(resolvedValue.toCharArray());
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
String varName = null;
String methodName = null;
String declaredType = null;
String sourceMethod = null;
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
methodName = mi.getName().getIdentifier();
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
declaredType = ce.getType().toString();
sourceMethod = "inline-cast";
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
} else {
// Fallback for complex chained expressions
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
if (!exprStr.contains("(")) {
varName = exprStr;
}
String methodName = resolvedValue.substring(lastDot + 1, openParen);
String declaredType = null;
String sourceMethod = null;
if (varName != null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
}
}
} else {
String baseExpr = resolvedValue.substring(0, lastDot);
if (baseExpr.contains("new ")) {
int newIdx = baseExpr.indexOf("new ");
int openParenBase = baseExpr.indexOf('(', newIdx);
if (openParenBase > newIdx) {
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
if (declaredType.contains("<")) {
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
}
sourceMethod = "inline-instantiation";
}
}
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType); // Track the payload type as the event
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ConditionalExpression cond) {
if (cond.getThenExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicThen) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType()));
}
if (cond.getElseExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicElse) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType()));
}
sourceMethod = "inline-ternary";
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
methodName = "VariableReference"; // We just want to trigger deep trace
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
}
if (methodName != null) {
if (varName != null && declaredType == null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
}
}
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
if (declaredType == null && varName.matches("^[A-Z].*")) {
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
if (staticTd != null) {
declaredType = context.getFqn(staticTd);
sourceMethod = "static-call";
}
}
}
if (declaredType != null) {
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
@@ -219,9 +261,6 @@ public class JdtCallGraphEngine implements CallGraphEngine {
}
}
}
}
// LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
// for string literals or enum-like constants.
boolean hasValidConstant = false;
@@ -263,7 +302,16 @@ public class JdtCallGraphEngine implements CallGraphEngine {
return !val.equals(val.toUpperCase()) || val.length() <= 2;
});
}
List<String> newPolyEvents = new ArrayList<>();
for (String pe : polymorphicEvents) {
List<String> resolved = resolveClassConstantReturns(pe, context, null);
if (resolved != null && !resolved.isEmpty()) {
newPolyEvents.addAll(resolved);
} else {
newPolyEvents.add(pe);
}
}
polymorphicEvents = newPolyEvents;
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
@@ -388,6 +436,10 @@ public class JdtCallGraphEngine implements CallGraphEngine {
} else {
constants.add(val);
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) {
extractConstantsFromExpression(ce, constants);
} else if (retExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
constants.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
@@ -409,6 +461,42 @@ public class JdtCallGraphEngine implements CallGraphEngine {
return constants;
}
private List<String> resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) {
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
if (td == null) td = context.getTypeDeclaration(className);
if (td == null) return null;
final List<String> resolvedConstants = new ArrayList<>();
for (MethodDeclaration md : td.getMethods()) {
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
extractConstantsFromExpression(node.getExpression(), resolvedConstants);
return super.visit(node);
}
});
}
if (!resolvedConstants.isEmpty()) return resolvedConstants;
}
return null;
}
private void extractConstantsFromExpression(org.eclipse.jdt.core.dom.Expression expr, List<String> constants) {
if (expr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
constants.add(sl.getLiteralValue());
} else if (expr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) {
extractConstantsFromExpression(ce.getThenExpression(), constants);
extractConstantsFromExpression(ce.getElseExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe) {
extractConstantsFromExpression(pe.getExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) {
extractConstantsFromExpression(ce.getExpression(), constants);
}
}
private String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));