Export linkResolution in JSON and tighten source-proven trigger scoping.
Wire enterprise regression tests for linkResolution, extend REST body enum detection to nested DTOs/records, infer if-branch source states only from provable literals, and fix String/java.lang.String routing equivalence so shared-service scoping stays fail-closed without dropping valid chains. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,7 +24,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
boolean mismatched = false;
|
||||
|
||||
if (triggerEventFqn != null && machineEventFqn != null) {
|
||||
if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) {
|
||||
if (typesEquivalent(triggerEventFqn, machineEventFqn)) {
|
||||
matched = true;
|
||||
} else if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||
mismatched = true;
|
||||
@@ -34,7 +34,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
}
|
||||
}
|
||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
||||
if (typesEquivalent(triggerStateFqn, machineStateFqn)) {
|
||||
matched = true;
|
||||
} else if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
||||
mismatched = true;
|
||||
@@ -322,6 +322,27 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
return type;
|
||||
}
|
||||
|
||||
private boolean typesEquivalent(String type1, String type2) {
|
||||
if (type1 == null || type2 == null) {
|
||||
return false;
|
||||
}
|
||||
String erased1 = normalizePrimitiveFqn(eraseGenerics(type1));
|
||||
String erased2 = normalizePrimitiveFqn(eraseGenerics(type2));
|
||||
return erased1.equals(erased2);
|
||||
}
|
||||
|
||||
private String normalizePrimitiveFqn(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (type) {
|
||||
case "java.lang.String" -> "String";
|
||||
case "java.lang.Object" -> "Object";
|
||||
case "java.io.Serializable" -> "Serializable";
|
||||
default -> type;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isTypeMismatched(String type1, String type2) {
|
||||
if (type1 == null || type2 == null) return false;
|
||||
type1 = eraseGenerics(type1);
|
||||
|
||||
@@ -5,6 +5,8 @@ import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Single source-derived policy for marking REST triggers as external vs internal.
|
||||
@@ -173,6 +175,9 @@ public final class ExternalTriggerPolicy {
|
||||
if (isEnumDtoField(bodyBinding, fieldName, context)) {
|
||||
return new RestParamBinding(false, false, true, true);
|
||||
}
|
||||
if (containsNestedEnumField(bodyBinding, fieldName, context)) {
|
||||
return new RestParamBinding(false, false, true, true);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -218,6 +223,38 @@ public final class ExternalTriggerPolicy {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean containsNestedEnumField(
|
||||
ITypeBinding bodyBinding, String fieldName, CodebaseContext context) {
|
||||
return containsNestedEnumField(bodyBinding, fieldName, context, new HashSet<>());
|
||||
}
|
||||
|
||||
private static boolean containsNestedEnumField(
|
||||
ITypeBinding bodyBinding,
|
||||
String fieldName,
|
||||
CodebaseContext context,
|
||||
Set<String> visited) {
|
||||
if (bodyBinding == null || fieldName == null || context == null) {
|
||||
return false;
|
||||
}
|
||||
String bodyFqn = bodyBinding.getQualifiedName();
|
||||
if (bodyFqn == null || !visited.add(bodyFqn)) {
|
||||
return false;
|
||||
}
|
||||
for (IVariableBinding component : bodyBinding.getDeclaredFields()) {
|
||||
ITypeBinding nestedType = component.getType();
|
||||
if (nestedType == null || nestedType.isPrimitive() || nestedType.isEnum()) {
|
||||
continue;
|
||||
}
|
||||
if (isEnumRecordComponent(nestedType, fieldName) || isEnumDtoField(nestedType, fieldName, context)) {
|
||||
return true;
|
||||
}
|
||||
if (containsNestedEnumField(nestedType, fieldName, context, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean hasParameterAstAnnotation(SingleVariableDeclaration param, String simpleName) {
|
||||
for (Object modifier : param.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation
|
||||
|
||||
@@ -477,20 +477,15 @@ public class GenericEventDetector {
|
||||
if (isRoutingParameter(left) || isRoutingParameter(right)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usually one is a method call like getState() or a variable like `state`
|
||||
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
|
||||
|
||||
// If one is a QualifiedName (enum constant) or StringLiteral, it's likely the state
|
||||
if (left instanceof QualifiedName || left instanceof StringLiteral || left instanceof FieldAccess) {
|
||||
return getSimpleNameString(left);
|
||||
|
||||
String leftState = resolveProvableStateLiteral(left);
|
||||
if (leftState != null) {
|
||||
return leftState;
|
||||
}
|
||||
if (right instanceof QualifiedName || right instanceof StringLiteral || right instanceof FieldAccess) {
|
||||
return getSimpleNameString(right);
|
||||
String rightState = resolveProvableStateLiteral(right);
|
||||
if (rightState != null) {
|
||||
return rightState;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return getSimpleNameString(right);
|
||||
}
|
||||
} else if (expr instanceof MethodInvocation mi) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
@@ -501,22 +496,12 @@ public class GenericEventDetector {
|
||||
if (isRoutingParameter(receiver) || isRoutingParameter(arg)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If receiver is null (e.g., implicit this), fall back to arg
|
||||
if (receiver == null) {
|
||||
return getSimpleNameString(arg);
|
||||
|
||||
String receiverState = resolveProvableStateLiteral(receiver);
|
||||
if (receiverState != null) {
|
||||
return receiverState;
|
||||
}
|
||||
|
||||
// Prioritize the one that looks like a constant (QualifiedName or StringLiteral)
|
||||
if (receiver instanceof QualifiedName || receiver instanceof StringLiteral || receiver instanceof FieldAccess) {
|
||||
return getSimpleNameString(receiver);
|
||||
}
|
||||
if (arg instanceof QualifiedName || arg instanceof StringLiteral || arg instanceof FieldAccess) {
|
||||
return getSimpleNameString(arg);
|
||||
}
|
||||
|
||||
// Fallback to receiver
|
||||
return getSimpleNameString(receiver);
|
||||
return resolveProvableStateLiteral(arg);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user