Scope triggers by @Qualifier bean name and infer source state from switch expressions.
Extract stateMachineId from @Qualifier on sendEvent receivers, route named beans via @EnableStateMachine before String/String type widening, and extend sourceState detection to SwitchExpression arrow cases. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,9 @@ package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.Annotation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.HashSet;
|
||||
@@ -12,8 +14,21 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
|
||||
@Override
|
||||
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||
String explicitTarget = chain.getContextMachineId();
|
||||
if (explicitTarget == null && chain.getTriggerPoint() != null) {
|
||||
explicitTarget = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
if (explicitTarget != null && !explicitTarget.isEmpty() && context != null && currentMachineName != null) {
|
||||
Boolean namedTarget = resolveNamedBeanTarget(explicitTarget, currentMachineName, context);
|
||||
if (namedTarget != null) {
|
||||
return namedTarget;
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasExplicitBeanTarget = explicitTarget != null && !explicitTarget.isEmpty();
|
||||
|
||||
// Precise FQN Type argument match from JDT bindings at sendEvent site
|
||||
if (chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
|
||||
if (!hasExplicitBeanTarget && chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
|
||||
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
||||
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
||||
if (triggerEventFqn != null || triggerStateFqn != null) {
|
||||
@@ -54,7 +69,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
}
|
||||
}
|
||||
|
||||
String targetVar = chain.getContextMachineId();
|
||||
String targetVar = hasExplicitBeanTarget ? explicitTarget : chain.getContextMachineId();
|
||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
@@ -129,6 +144,67 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
return context == null || isSingleStateMachine(context);
|
||||
}
|
||||
|
||||
private Boolean resolveNamedBeanTarget(String beanName, String currentMachineName, CodebaseContext context) {
|
||||
String normalized = stripAnnotationQuotes(beanName);
|
||||
if (normalized == null || normalized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String matchingConfig = null;
|
||||
for (TypeDeclaration configType : context.findEntryPointClasses(java.util.List.of("EnableStateMachine"))) {
|
||||
String configFqn = context.getFqn(configType);
|
||||
String configuredName = extractEnableStateMachineName(configType);
|
||||
if (configuredName == null || configuredName.isEmpty()) {
|
||||
configuredName = defaultBeanName(configType.getName().getIdentifier());
|
||||
}
|
||||
if (normalized.equals(configuredName)) {
|
||||
matchingConfig = configFqn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matchingConfig == null) {
|
||||
return null;
|
||||
}
|
||||
return currentMachineName.equals(matchingConfig);
|
||||
}
|
||||
|
||||
private static String extractEnableStateMachineName(TypeDeclaration configType) {
|
||||
for (Object modifierObj : configType.modifiers()) {
|
||||
if (!(modifierObj instanceof Annotation annotation)) {
|
||||
continue;
|
||||
}
|
||||
if (!annotation.getTypeName().toString().endsWith("EnableStateMachine")) {
|
||||
continue;
|
||||
}
|
||||
String name = AstUtils.extractAnnotationMember(annotation, "name");
|
||||
if (name != null && !name.isBlank()) {
|
||||
return stripAnnotationQuotes(name);
|
||||
}
|
||||
String value = AstUtils.extractAnnotationMember(annotation, "value");
|
||||
if (value != null && !value.isBlank()) {
|
||||
return stripAnnotationQuotes(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String defaultBeanName(String simpleClassName) {
|
||||
if (simpleClassName == null || simpleClassName.isEmpty()) {
|
||||
return simpleClassName;
|
||||
}
|
||||
return Character.toLowerCase(simpleClassName.charAt(0)) + simpleClassName.substring(1);
|
||||
}
|
||||
|
||||
private static String stripAnnotationQuotes(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = raw.trim();
|
||||
if (trimmed.length() >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
||||
return trimmed.substring(1, trimmed.length() - 1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private int countStateMachines(CodebaseContext context) {
|
||||
if (context == null) {
|
||||
return 0;
|
||||
|
||||
@@ -97,6 +97,7 @@ public class GenericEventDetector {
|
||||
if (sourceState == null) {
|
||||
sourceState = extractSourceStateFromArguments(node);
|
||||
}
|
||||
String stateMachineId = extractStateMachineId(node);
|
||||
String[] smTypes = resolveStateMachineTypeArgumentsForExpression(emr.getExpression(), node);
|
||||
|
||||
boolean external = false;
|
||||
@@ -110,6 +111,7 @@ public class GenericEventDetector {
|
||||
triggers.add(TriggerPoint.builder()
|
||||
.event(eventValue)
|
||||
.sourceState(sourceState)
|
||||
.stateMachineId(stateMachineId)
|
||||
.className(context.getFqn(type))
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
@@ -311,6 +313,7 @@ public class GenericEventDetector {
|
||||
if (sourceState == null && node instanceof MethodInvocation mi) {
|
||||
sourceState = extractSourceStateFromArguments(mi);
|
||||
}
|
||||
String stateMachineId = extractStateMachineId(node);
|
||||
String[] smTypes = resolveStateMachineTypeArguments(node);
|
||||
|
||||
boolean external = false;
|
||||
@@ -330,6 +333,7 @@ public class GenericEventDetector {
|
||||
results.add(TriggerPoint.builder()
|
||||
.event(part.trim())
|
||||
.sourceState(sourceState)
|
||||
.stateMachineId(stateMachineId)
|
||||
.className(context.getFqn(type))
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
@@ -345,6 +349,7 @@ public class GenericEventDetector {
|
||||
results.add(TriggerPoint.builder()
|
||||
.event(eventValue)
|
||||
.sourceState(sourceState)
|
||||
.stateMachineId(stateMachineId)
|
||||
.className(context.getFqn(type))
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
@@ -383,6 +388,21 @@ public class GenericEventDetector {
|
||||
String state = extractStateFromSiblings(current, switchStmt.statements());
|
||||
if (state != null) return state;
|
||||
}
|
||||
} else if (parent instanceof SwitchExpression switchExpr) {
|
||||
if (!isRoutingParameter(switchExpr.getExpression())) {
|
||||
String state = extractStateFromSiblings(current, switchExpr.statements());
|
||||
if (state != null) return state;
|
||||
}
|
||||
} else if (parent instanceof SwitchCase switchCase && !switchCase.isDefault()) {
|
||||
Expression selector = resolveSwitchSelector(switchCase);
|
||||
if (selector != null && !isRoutingParameter(selector) && !switchCase.expressions().isEmpty()) {
|
||||
Expression caseExpr = (Expression) switchCase.expressions().get(0);
|
||||
String resolved = resolveProvableStateLiteral(caseExpr);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
return getSimpleNameString(caseExpr);
|
||||
}
|
||||
}
|
||||
|
||||
current = parent;
|
||||
@@ -390,6 +410,151 @@ public class GenericEventDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression resolveSwitchSelector(SwitchCase switchCase) {
|
||||
ASTNode switchParent = switchCase.getParent();
|
||||
if (switchParent instanceof SwitchStatement switchStatement) {
|
||||
return switchStatement.getExpression();
|
||||
}
|
||||
if (switchParent instanceof SwitchExpression switchExpression) {
|
||||
return switchExpression.getExpression();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractStateMachineId(MethodInvocation node) {
|
||||
if (!TRIGGER_METHOD_NAMES.contains(node.getName().getIdentifier())) {
|
||||
return null;
|
||||
}
|
||||
Expression receiver = node.getExpression();
|
||||
while (receiver instanceof MethodInvocation methodInvocation) {
|
||||
receiver = methodInvocation.getExpression();
|
||||
}
|
||||
if (receiver == null) {
|
||||
return null;
|
||||
}
|
||||
AbstractTypeDeclaration enclosing = findEnclosingAbstractType(node);
|
||||
if (enclosing == null) {
|
||||
return null;
|
||||
}
|
||||
return resolveQualifierForReceiver(receiver, enclosing);
|
||||
}
|
||||
|
||||
private String resolveQualifierForReceiver(Expression receiver, AbstractTypeDeclaration enclosing) {
|
||||
String fieldName = null;
|
||||
if (receiver instanceof SimpleName simpleName) {
|
||||
fieldName = simpleName.getIdentifier();
|
||||
IBinding binding = simpleName.resolveBinding();
|
||||
if (binding instanceof IVariableBinding variableBinding) {
|
||||
String fromBinding = extractQualifierFromBinding(variableBinding);
|
||||
if (fromBinding != null) {
|
||||
return fromBinding;
|
||||
}
|
||||
}
|
||||
} else if (receiver instanceof FieldAccess fieldAccess) {
|
||||
fieldName = fieldAccess.getName().getIdentifier();
|
||||
}
|
||||
if (fieldName == null) {
|
||||
return null;
|
||||
}
|
||||
return findQualifierOnField(enclosing, fieldName);
|
||||
}
|
||||
|
||||
private String findQualifierOnField(AbstractTypeDeclaration enclosing, String fieldName) {
|
||||
for (FieldDeclaration fieldDeclaration : getFieldDeclarations(enclosing)) {
|
||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
||||
&& fragment.getName().getIdentifier().equals(fieldName)) {
|
||||
String qualifier = extractQualifierAnnotation(fieldDeclaration.modifiers());
|
||||
if (qualifier != null) {
|
||||
return qualifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractQualifierFromBinding(IVariableBinding binding) {
|
||||
if (binding == null) {
|
||||
return null;
|
||||
}
|
||||
String qualifier = readQualifierFromAnnotations(binding.getAnnotations());
|
||||
if (qualifier != null) {
|
||||
return qualifier;
|
||||
}
|
||||
if (binding.isField() && binding.getDeclaringClass() != null) {
|
||||
for (IMethodBinding method : binding.getDeclaringClass().getDeclaredMethods()) {
|
||||
if (!method.isConstructor()) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < method.getParameterTypes().length; i++) {
|
||||
if (method.getParameterTypes()[i].isEqualTo(binding.getType().getErasure())
|
||||
&& binding.getName().equals(method.getParameterNames()[i])) {
|
||||
String paramQualifier = readQualifierFromAnnotations(method.getParameterAnnotations(i));
|
||||
if (paramQualifier != null) {
|
||||
return paramQualifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String readQualifierFromAnnotations(IAnnotationBinding[] annotations) {
|
||||
if (annotations == null) {
|
||||
return null;
|
||||
}
|
||||
for (IAnnotationBinding annotation : annotations) {
|
||||
if (annotation.getAnnotationType() == null
|
||||
|| !"org.springframework.beans.factory.annotation.Qualifier"
|
||||
.equals(annotation.getAnnotationType().getQualifiedName())) {
|
||||
continue;
|
||||
}
|
||||
for (IMemberValuePairBinding pair : annotation.getDeclaredMemberValuePairs()) {
|
||||
Object value = pair.getValue();
|
||||
if (value instanceof String stringValue) {
|
||||
return stringValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractQualifierAnnotation(List<?> modifiers) {
|
||||
if (modifiers == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object modifierObj : modifiers) {
|
||||
if (!(modifierObj instanceof Annotation annotation)) {
|
||||
continue;
|
||||
}
|
||||
String typeName = annotation.getTypeName().toString();
|
||||
if (!typeName.endsWith("Qualifier")) {
|
||||
continue;
|
||||
}
|
||||
String value = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(
|
||||
annotation, "value");
|
||||
if (value == null || value.isBlank()) {
|
||||
value = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(
|
||||
annotation, "name");
|
||||
}
|
||||
return stripAnnotationQuotes(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String stripAnnotationQuotes(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = raw.trim();
|
||||
if (trimmed.length() >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
||||
return trimmed.substring(1, trimmed.length() - 1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers source state from a literal second argument to sendEvent(event, sourceState) when provable from AST.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user