Align enricher pipeline with context-aware enum canonicalization

Use label-only canonicalization for shared-infrastructure triggers, resolve valueOf enum types from JDT bindings when available, and allow trusted valueOf widens using the machine event type when the trigger omits type FQNs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 06:13:34 +02:00
parent 5c0d9c466d
commit 5bc65be30d
6 changed files with 112 additions and 10 deletions

View File

@@ -20,6 +20,12 @@ public final class CallChainLinkPolicy {
}
public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(TriggerPoint trigger) {
return shouldFailClosedOnAmbiguousCallGraphWiden(trigger, null);
}
public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(
TriggerPoint trigger,
String machineEventTypeFqn) {
if (trigger == null || trigger.isExternal() || !trigger.isAmbiguous()) {
return false;
}
@@ -33,7 +39,7 @@ public final class CallChainLinkPolicy {
}
if (event != null && event.contains(".valueOf(")) {
return MachineEnumCanonicalizer.isDynamicTriggerExpression(event)
&& !isTrustedEnumPolymorphicWiden(trigger);
&& !isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn);
}
return MachineEnumCanonicalizer.isDynamicTriggerExpression(event)
&& event != null;
@@ -45,10 +51,17 @@ public final class CallChainLinkPolicy {
* cross-package or unqualified widens.
*/
static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger) {
return isTrustedEnumPolymorphicWiden(trigger, null);
}
static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger, String machineEventTypeFqn) {
if (trigger == null) {
return false;
}
String eventTypeFqn = trigger.getEventTypeFqn();
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
eventTypeFqn = machineEventTypeFqn;
}
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
return false;
}

View File

@@ -68,7 +68,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
}
String triggerSource = tp.getSourceState() != null ? simplifySourceState(tp.getSourceState()) : null;
if (CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(tp)) {
if (CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
tp, machineTypes != null ? machineTypes.eventTypeFqn() : null)) {
updatedChains.add(chain.toBuilder()
.triggerPoint(tp)
.matchedTransitions(null)

View File

@@ -45,11 +45,14 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
if (chain.getTriggerPoint() == null) {
return chain;
}
TriggerPoint canonical = MachineEnumCanonicalizer.canonicalizeTriggerPoint(
chain.getTriggerPoint(), machineTypes);
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
canonical, machineTypes, context, result.getTransitions(), true);
return chain.toBuilder().triggerPoint(enriched).build();
TriggerPoint expanded = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
chain.getTriggerPoint(), machineTypes, context, result.getTransitions(), true);
TriggerPoint canonical = expanded.getEventTypeFqn() == null && expanded.getStateTypeFqn() == null
? MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(
expanded, machineTypes, context)
: MachineEnumCanonicalizer.canonicalizeTriggerPoint(
expanded, machineTypes, context);
return chain.toBuilder().triggerPoint(canonical).build();
})
.collect(Collectors.toList());

View File

@@ -2756,16 +2756,52 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
private String resolveValueOfEnumTypeName(MethodInvocation mi) {
if (mi.getExpression() instanceof SimpleName sn) {
if (mi == null || !"valueOf".equals(mi.getName().getIdentifier())) {
return null;
}
org.eclipse.jdt.core.dom.IMethodBinding binding = mi.resolveMethodBinding();
if (binding != null && binding.getDeclaringClass() != null) {
String fqn = binding.getDeclaringClass().getErasure().getQualifiedName();
if (isResolvedEnumTypeFqn(fqn)) {
return fqn;
}
}
Expression receiver = mi.getExpression();
if (receiver instanceof SimpleName sn) {
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = sn.resolveTypeBinding();
if (typeBinding != null) {
String fqn = typeBinding.getErasure().getQualifiedName();
if (isResolvedEnumTypeFqn(fqn)) {
return fqn;
}
}
return sn.getIdentifier();
}
if (mi.getExpression() instanceof QualifiedName qn) {
if (receiver instanceof QualifiedName qn) {
org.eclipse.jdt.core.dom.IBinding nameBinding = qn.resolveBinding();
if (nameBinding instanceof org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
String fqn = typeBinding.getErasure().getQualifiedName();
if (isResolvedEnumTypeFqn(fqn)) {
return fqn;
}
}
String full = qn.getFullyQualifiedName();
return full.contains(".") ? full.substring(full.lastIndexOf('.') + 1) : full;
if (full.contains(".")) {
String typePart = full.substring(0, full.lastIndexOf('.'));
if (isResolvedEnumTypeFqn(typePart)) {
return typePart;
}
return full.substring(full.lastIndexOf('.') + 1);
}
return full;
}
return null;
}
private static boolean isResolvedEnumTypeFqn(String fqn) {
return fqn != null && !fqn.isBlank() && fqn.contains(".") && !fqn.startsWith("<");
}
private String resolveEnumConstantFromArgument(String argName, List<String> path, Map<String, List<CallEdge>> callGraph) {
if (argName == null || path == null || path.size() < 2) {
return null;

View File

@@ -60,6 +60,20 @@ class CallChainLinkPolicyTest {
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(trigger)).isFalse();
}
@Test
void shouldAllowTrustedWidenUsingMachineEventTypeWhenTriggerTypeIsNull() {
TriggerPoint trigger = TriggerPoint.builder()
.ambiguous(true)
.event("OrderEvent.valueOf(eventStr)")
.polymorphicEvents(List.of("com.example.OrderEvent.PAY", "com.example.OrderEvent.SHIP"))
.build();
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(
trigger, "com.example.OrderEvent")).isTrue();
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
trigger, "com.example.OrderEvent")).isFalse();
}
@Test
void shouldNotTrustImportStylePolymorphicWidenWithoutPackage() {
TriggerPoint trigger = TriggerPoint.builder()

View File

@@ -1,6 +1,7 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
@@ -224,5 +225,39 @@ class AmbiguousSimpleEnumLinkingTest {
assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
assertThat(linked.getLinkResolution()).isIn(LinkResolution.NO_MATCH, LinkResolution.AMBIGUOUS_WIDEN);
}
@Test
void shouldNotRewriteAmbiguousImportStylePolyDuringTriggerCanonicalization(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
"package a; public enum OrderEvent { PAY }");
Files.writeString(tempDir.resolve("b/OrderEvent.java"),
"package b; public enum OrderEvent { CANCEL }");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TriggerPoint trigger = TriggerPoint.builder()
.event("OrderEvent.valueOf(eventStr)")
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
.ambiguous(true)
.build();
AnalysisResult result = AnalysisResult.builder()
.name("com.example.OrderStateMachineConfig")
.eventTypeFqn("a.OrderEvent")
.transitions(List.of())
.metadata(CodebaseMetadata.builder()
.callChains(List.of(CallChain.builder().triggerPoint(trigger).build()))
.build())
.build();
new TriggerCanonicalizationEnricher().enrich(result, context, null);
TriggerPoint canonical = result.getMetadata().getCallChains().get(0).getTriggerPoint();
assertThat(canonical.getPolymorphicEvents())
.containsExactly("OrderEvent.PAY", "OrderEvent.SHIP");
}
}