update argument resolution to
This commit is contained in:
@@ -59,7 +59,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
.targetState(smSourceRaw)
|
.targetState(smSourceRaw)
|
||||||
.event(smEventRaw)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
|
||||||
matched.add(mt);
|
matched.add(mt);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
.targetState(targetRaw)
|
.targetState(targetRaw)
|
||||||
.event(smEventRaw)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
|
||||||
matched.add(mt);
|
matched.add(mt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,8 +100,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||||
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
|
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String simplify(String name) {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
String simplifiedPe = simplify(simplePe);
|
String simplifiedPe = simplify(simplePe);
|
||||||
|
|
||||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
||||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
|
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) ||
|
||||||
|
isGuardedContains(smEvent, simplifiedPe)) {
|
||||||
hasPolyMatch = true;
|
hasPolyMatch = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,38 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String triggerEvent = simplify(rawTriggerEvent);
|
String triggerEvent = simplify(rawTriggerEvent);
|
||||||
return smEvent.equals(triggerEvent);
|
if (smEvent.equalsIgnoreCase(triggerEvent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isGuardedContains(smEvent, triggerEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
|
if (smEvent == null || triggerEvent == null) return false;
|
||||||
|
// Only fire if it looks like a simple identifier (no spaces, no parens)
|
||||||
|
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
|
||||||
|
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
|
||||||
|
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String smLower = smEvent.toLowerCase();
|
||||||
|
String triggerLower = triggerEvent.toLowerCase();
|
||||||
|
|
||||||
|
if (smLower.equals(triggerLower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match if one contains the other as a whole word/segment.
|
||||||
|
// We quote the patterns to avoid regex syntax injection issues.
|
||||||
|
String pattern1 = "\\b" + java.util.regex.Pattern.quote(smLower) + "\\b";
|
||||||
|
String pattern2 = "\\b" + java.util.regex.Pattern.quote(triggerLower) + "\\b";
|
||||||
|
|
||||||
|
// Treat underscores and hyphens as word boundary separators
|
||||||
|
String smNormalized = smLower.replace('_', ' ').replace('-', ' ');
|
||||||
|
String triggerNormalized = triggerLower.replace('_', ' ').replace('-', ' ');
|
||||||
|
|
||||||
|
return smNormalized.matches(".*" + pattern2 + ".*") || triggerNormalized.matches(".*" + pattern1 + ".*");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWildcardVariable(String eventStr) {
|
private boolean isWildcardVariable(String eventStr) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
public interface BeanResolutionEngine {
|
public interface BeanResolutionEngine {
|
||||||
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName);
|
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -8,7 +9,35 @@ import java.util.Set;
|
|||||||
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||||
|
// Precise FQN Type argument match
|
||||||
|
if (chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
|
||||||
|
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
||||||
|
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
||||||
|
if (triggerEventFqn != null || triggerStateFqn != null) {
|
||||||
|
String[] machineTypes = getStateMachineTypeArguments(currentMachineName, context);
|
||||||
|
String machineStateFqn = machineTypes[0];
|
||||||
|
String machineEventFqn = machineTypes[1];
|
||||||
|
|
||||||
|
if (triggerEventFqn != null && machineEventFqn != null) {
|
||||||
|
if (triggerEventFqn.equals(machineEventFqn)) {
|
||||||
|
return true; // Match!
|
||||||
|
}
|
||||||
|
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||||
|
return false; // Mismatch!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||||
|
if (triggerStateFqn.equals(machineStateFqn)) {
|
||||||
|
return true; // Match!
|
||||||
|
}
|
||||||
|
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
||||||
|
return false; // Mismatch!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String targetVar = chain.getContextMachineId();
|
String targetVar = chain.getContextMachineId();
|
||||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||||
@@ -119,41 +148,40 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
|
|
||||||
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
||||||
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
|
if (smPrefix == null || chainPrefix == null) return false;
|
||||||
|
|
||||||
String[] p1 = smPackage.split("\\.");
|
String smLower = smPrefix.toLowerCase();
|
||||||
String[] p2 = chainPackage.split("\\.");
|
String chainLower = chainPrefix.toLowerCase();
|
||||||
|
|
||||||
int matchIdx = -1;
|
// Ignore generic/technical class prefixes
|
||||||
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") || smLower.equals("configuration") || smLower.equals("adapter") ||
|
||||||
if (p1[i].equals(p2[i])) {
|
chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter")) {
|
||||||
matchIdx = i;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchIdx < 1) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> smDivergentTerms = new HashSet<>();
|
// If class prefixes diverge, check if they are from different domains
|
||||||
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
|
if (!smLower.equals(chainLower)) {
|
||||||
for (int i = matchIdx + 1; i < p1.length; i++) {
|
// If one prefix is present as a domain/feature segment in the other package, they are related
|
||||||
smDivergentTerms.add(p1[i].toLowerCase());
|
if (chainPackage.toLowerCase().contains(smLower) || smPackage.toLowerCase().contains(chainLower)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, if they diverge under a shared project package root, it is a mismatch
|
||||||
|
String[] p1 = smPackage.split("\\.");
|
||||||
|
String[] p2 = chainPackage.split("\\.");
|
||||||
|
int matchIdx = -1;
|
||||||
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
|
if (p1[i].equals(p2[i])) {
|
||||||
|
matchIdx = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchIdx >= 1) {
|
||||||
|
return true; // Diverges under a shared root
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> chainDivergentTerms = new HashSet<>();
|
|
||||||
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
|
|
||||||
for (int i = matchIdx + 1; i < p2.length; i++) {
|
|
||||||
chainDivergentTerms.add(p2[i].toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
|
|
||||||
Set<String> intersection = new HashSet<>(smDivergentTerms);
|
|
||||||
intersection.retainAll(chainDivergentTerms);
|
|
||||||
return intersection.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,4 +232,87 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] getStateMachineTypeArguments(String machineName, CodebaseContext context) {
|
||||||
|
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(machineName);
|
||||||
|
if (td != null) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = td.getSuperclassType();
|
||||||
|
return findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
|
||||||
|
}
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] findTypeArgumentsRecursively(org.eclipse.jdt.core.dom.Type type, CodebaseContext context, org.eclipse.jdt.core.dom.CompilationUnit cu) {
|
||||||
|
if (type == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
if (type instanceof org.eclipse.jdt.core.dom.ParameterizedType pt) {
|
||||||
|
String rawTypeName = pt.getType().toString();
|
||||||
|
if (rawTypeName.equals("StateMachineConfigurerAdapter") || rawTypeName.equals("EnumStateMachineConfigurerAdapter") ||
|
||||||
|
rawTypeName.endsWith(".StateMachineConfigurerAdapter") || rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
||||||
|
java.util.List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
String stateType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(0), cu, context);
|
||||||
|
String eventType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(1), cu, context);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into the raw type declaration's superclass
|
||||||
|
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(rawTypeName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
String[] res = findTypeArgumentsRecursively(td.getSuperclassType(), context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
|
||||||
|
if (res[0] != null || res[1] != null) return res;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a simple type (e.g. MyBaseConfig)
|
||||||
|
String simpleName = type.toString();
|
||||||
|
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
String[] res = findTypeArgumentsRecursively(td.getSuperclassType(), context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
|
||||||
|
if (res[0] != null || res[1] != null) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((org.eclipse.jdt.core.dom.SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((org.eclipse.jdt.core.dom.ParameterizedType) type).getType(), cu, context);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
org.eclipse.jdt.core.dom.ImportDeclaration imp = (org.eclipse.jdt.core.dom.ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
|
if (!packageName.isEmpty()) {
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||||
|
if (localTd != null) return context.getFqn(localTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTypeMismatched(String type1, String type2) {
|
||||||
|
if (type1 == null || type2 == null) return false;
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,9 @@ public class TriggerPoint {
|
|||||||
private final String stateMachineId; // Optional: to link to a specific SM instance
|
private final String stateMachineId; // Optional: to link to a specific SM instance
|
||||||
private final String sourceState; // Optional: if we can determine the expected current state
|
private final String sourceState; // Optional: if we can determine the expected current state
|
||||||
private final int lineNumber;
|
private final int lineNumber;
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
private final String stateTypeFqn; // Type of State (e.g. OrderStates FQN)
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
private final String eventTypeFqn; // Type of Event (e.g. OrderEvents FQN)
|
||||||
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ public class GenericEventDetector {
|
|||||||
if (type == null) return Collections.emptyList();
|
if (type == null) return Collections.emptyList();
|
||||||
|
|
||||||
String sourceState = extractSourceState(node);
|
String sourceState = extractSourceState(node);
|
||||||
|
String[] smTypes = resolveStateMachineTypeArguments(node);
|
||||||
|
|
||||||
List<TriggerPoint> results = new ArrayList<>();
|
List<TriggerPoint> results = new ArrayList<>();
|
||||||
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||||
@@ -154,6 +155,8 @@ public class GenericEventDetector {
|
|||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +168,8 @@ public class GenericEventDetector {
|
|||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,4 +415,135 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
return (TypeDeclaration) parent;
|
return (TypeDeclaration) parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] resolveStateMachineTypeArguments(MethodInvocation node) {
|
||||||
|
Expression receiver = node.getExpression();
|
||||||
|
if (receiver == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding smBinding = findStateMachineSupertype(binding);
|
||||||
|
if (smBinding != null) {
|
||||||
|
ITypeBinding[] typeArgs = smBinding.getTypeArguments();
|
||||||
|
if (typeArgs.length >= 2) {
|
||||||
|
return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: search enclosing class/method for variable declaration
|
||||||
|
Type declType = findReceiverTypeManually(receiver, node);
|
||||||
|
if (declType instanceof ParameterizedType pt) {
|
||||||
|
List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
CompilationUnit cu = (CompilationUnit) node.getRoot();
|
||||||
|
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu);
|
||||||
|
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ITypeBinding findStateMachineSupertype(ITypeBinding binding) {
|
||||||
|
ITypeBinding current = binding;
|
||||||
|
while (current != null) {
|
||||||
|
if (current.getErasure().getQualifiedName().equals("org.springframework.statemachine.StateMachine")) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
for (ITypeBinding iface : current.getInterfaces()) {
|
||||||
|
ITypeBinding res = findStateMachineSupertype(iface);
|
||||||
|
if (res != null) return res;
|
||||||
|
}
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type findReceiverTypeManually(Expression receiver, MethodInvocation node) {
|
||||||
|
if (!(receiver instanceof SimpleName sn)) return null;
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(node);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
// Check params
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return svd.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check local variables
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
Type[] found = new Type[1];
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(varName)) {
|
||||||
|
found[0] = vds.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (found[0] != null) return found[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check fields
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(node);
|
||||||
|
if (enclosingType != null) {
|
||||||
|
for (FieldDeclaration fd : enclosingType.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(varName)) {
|
||||||
|
return fd.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to import matching
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check package name
|
||||||
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
|
if (!packageName.isEmpty()) {
|
||||||
|
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||||
|
if (localTd != null) return context.getFqn(localTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,4 +88,22 @@ class HeuristicEventMatchingEngineTest {
|
|||||||
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build();
|
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build();
|
||||||
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
|
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchCaseInsensitive() {
|
||||||
|
Event smEvent = Event.of("PAY", "PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder().event("pay").build();
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchGuardedContains() {
|
||||||
|
Event smEvent = Event.of("PAY", "PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder().event("pay_order").build();
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
|
|
||||||
|
Event smEvent2 = Event.of("PAY_ORDER", "PAY_ORDER");
|
||||||
|
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("pay").build();
|
||||||
|
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
|
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// They share com.acme.corp.division.project, but diverge into orders vs payments
|
// They share com.acme.corp.division.project, but diverge into orders vs payments
|
||||||
// This should be a strong mismatch, so it should return false
|
// This should be a strong mismatch, so it should return false
|
||||||
@@ -37,7 +37,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Same exact domain, should match
|
// Same exact domain, should match
|
||||||
assertTrue(result, "Same domain should be accepted");
|
assertTrue(result, "Same domain should be accepted");
|
||||||
@@ -51,7 +51,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Subpackage should match
|
// Subpackage should match
|
||||||
assertTrue(result, "Sub-packages of the same domain should be accepted");
|
assertTrue(result, "Sub-packages of the same domain should be accepted");
|
||||||
@@ -65,7 +65,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.orders.service.OrderStateMachine";
|
String machineName = "com.acme.orders.service.OrderStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Different subpackages, but they share the "order" domain term and common prefix
|
// Different subpackages, but they share the "order" domain term and common prefix
|
||||||
assertTrue(result, "Divergent packages that share a domain term should be accepted");
|
assertTrue(result, "Divergent packages that share a domain term should be accepted");
|
||||||
@@ -80,7 +80,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.corp.payments.PaymentStateMachine";
|
String machineName = "com.acme.corp.payments.PaymentStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Mismatched domains, but explicit variable targeting works
|
// Mismatched domains, but explicit variable targeting works
|
||||||
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
|
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
|
||||||
@@ -95,9 +95,24 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
|
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Explicit variable targeting completely conflicts
|
// Explicit variable targeting completely conflicts
|
||||||
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
|
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenericStateMachineConfigInGenericPackage() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.corp.orders.OrderController.submit()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.corp.config.StateMachineConfig";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
|
// StateMachineConfig has generic prefix and is in config package. OrderController is in orders.
|
||||||
|
// There should be no domain mismatch because config/statemachine are generic.
|
||||||
|
assertTrue(result, "Generic state machine config in generic config package should not trigger mismatch");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class EnterpriseBugsTest {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted
|
// Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted
|
||||||
boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine");
|
boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine", null);
|
||||||
assertThat(isMatched).isTrue();
|
assertThat(isMatched).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,4 +120,52 @@ class EnterpriseBugsTest {
|
|||||||
// Assert Bug 6: resolves cleanly to simple name (since prefix is a known type)
|
// Assert Bug 6: resolves cleanly to simple name (since prefix is a known type)
|
||||||
assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp");
|
assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchFqnsAndMismatchesOnRouting(@TempDir Path tempDir) throws IOException {
|
||||||
|
String statesSrc = """
|
||||||
|
package com.example;
|
||||||
|
public enum OrderStates { S1, S2 }
|
||||||
|
""";
|
||||||
|
String eventsSrc = """
|
||||||
|
package com.example;
|
||||||
|
public enum OrderEvents { E1, E2 }
|
||||||
|
""";
|
||||||
|
String otherEventsSrc = """
|
||||||
|
package com.example.other;
|
||||||
|
public enum OtherEvents { E1, E2 }
|
||||||
|
""";
|
||||||
|
String configSrc = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderStateMachineConfig extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderStates, OrderEvents> {}
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("OrderStates.java"), statesSrc);
|
||||||
|
Files.writeString(tempDir.resolve("OrderEvents.java"), eventsSrc);
|
||||||
|
Files.writeString(tempDir.resolve("OtherEvents.java"), otherEventsSrc);
|
||||||
|
Files.writeString(tempDir.resolve("OrderStateMachineConfig.java"), configSrc);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
||||||
|
|
||||||
|
// Scenario 1: Exact Event type argument match
|
||||||
|
CallChain chain1 = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.eventTypeFqn("com.example.OrderEvents")
|
||||||
|
.className("com.example.Service")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
assertThat(engine.isRoutedToCorrectMachine(chain1, "com.example.OrderStateMachineConfig", context)).isTrue();
|
||||||
|
|
||||||
|
// Scenario 2: Event type argument mismatch
|
||||||
|
CallChain chain2 = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.eventTypeFqn("com.example.other.OtherEvents")
|
||||||
|
.className("com.example.Service")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
assertThat(engine.isRoutedToCorrectMachine(chain2, "com.example.OrderStateMachineConfig", context)).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user