From 26560fee6d14488ae01964bf171fdd7c4f9e9725 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 05:51:03 +0200 Subject: [PATCH] Fail closed on ambiguous enum type matching during linking Pass CodebaseContext into StrictFqnMatchingEngine so import-style OrderEvent.PAY cannot match a.OrderEvent.PAY when OrderEvent is ambiguous across packages. Co-authored-by: Cursor --- .../enricher/TransitionLinkerEnricher.java | 3 ++- .../matching/StrictFqnMatchingEngine.java | 21 +++++++++++++--- .../HeuristicBeanResolutionEngine.java | 11 +++++---- .../resolver/MachineEnumCanonicalizer.java | 11 +++++++++ .../matching/StrictFqnMatchingEngineTest.java | 24 +++++++++++++++++++ 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index 953254f..67ce825 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -32,7 +32,6 @@ import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineType public class TransitionLinkerEnricher implements AnalysisEnricher { private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine(); - private final EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(); @Override public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { @@ -40,6 +39,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { return; } + EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(context); + List updatedChains = new ArrayList<>(); List stateMachineTransitions = result.getTransitions(); StateMachineTypeResolver.MachineTypes machineTypes = diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java index 4baeea7..a7032c1 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java @@ -2,11 +2,22 @@ package click.kamil.springstatemachineexporter.analysis.enricher.matching; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.model.Event; import java.util.List; public class StrictFqnMatchingEngine implements EventMatchingEngine { + private final CodebaseContext context; + + public StrictFqnMatchingEngine() { + this(null); + } + + public StrictFqnMatchingEngine(CodebaseContext context) { + this.context = context; + } + @Override public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) { if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) { @@ -52,7 +63,7 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine { if (pe.equals(smConst) && triggerPoint.getEventTypeFqn() != null && !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) { String smEnumType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.')); - return MachineEnumCanonicalizer.enumTypesMatch(triggerPoint.getEventTypeFqn(), smEnumType); + return typesMatch(triggerPoint.getEventTypeFqn(), smEnumType); } } } @@ -103,12 +114,12 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine { String smEnumType = smEvent.substring(0, smEvent.lastIndexOf('.')); if (eventTypeFqn != null) { - return MachineEnumCanonicalizer.enumTypesMatch(eventTypeFqn, smEnumType); + return typesMatch(eventTypeFqn, smEnumType); } if (triggerEvent.contains(".")) { String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.')); - return MachineEnumCanonicalizer.enumTypesMatch(triggerEnumType, smEnumType); + return typesMatch(triggerEnumType, smEnumType); } // Bare constant name (e.g. after Enum.name()) with no type context must not match every enum @@ -119,6 +130,10 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine { return !triggerEvent.contains("."); } + private boolean typesMatch(String type1, String type2) { + return MachineEnumCanonicalizer.enumTypesMatch(type1, type2, context); + } + private String constantName(String event) { return event.contains(".") ? event.substring(event.lastIndexOf('.') + 1) : event; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java index c4f4d22..f65b601 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter.analysis.enricher.routing; +import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine; import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine; import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; @@ -14,8 +15,6 @@ import java.util.Set; public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { - private final StrictFqnMatchingEngine matchingEngine = new StrictFqnMatchingEngine(); - private enum DistinctTypeAffinity { FOR_MACHINE, AGAINST_MACHINE, @@ -64,7 +63,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { TriggerPoint trigger = chain.getTriggerPoint(); if (trigger != null && SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain) - && matchesConfiguredTransitionEvent(trigger, machineTransitions)) { + && matchesConfiguredTransitionEvent(trigger, machineTransitions, context)) { return true; } @@ -189,10 +188,14 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { return DistinctTypeAffinity.UNKNOWN; } - private boolean matchesConfiguredTransitionEvent(TriggerPoint trigger, List machineTransitions) { + private boolean matchesConfiguredTransitionEvent( + TriggerPoint trigger, + List machineTransitions, + CodebaseContext context) { if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) { return false; } + EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(context); for (Transition transition : machineTransitions) { if (transition.getEvent() != null && matchingEngine.matches(transition.getEvent(), trigger)) { return true; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java index e8e6bd2..3c45116 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java @@ -809,6 +809,17 @@ public final class MachineEnumCanonicalizer { return !type1.contains(".") || !type2.contains("."); } + public static boolean enumTypesMatch(String type1, String type2, CodebaseContext context) { + if (context != null && type1 != null && type2 != null) { + String simple1 = simpleName(type1); + String simple2 = simpleName(type2); + if (simple1.equals(simple2) && context.isAmbiguousSimpleName(simple1)) { + return type1.equals(type2); + } + } + return enumTypesMatch(type1, type2); + } + private static String simpleName(String fqn) { return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn; } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngineTest.java index 0b266fe..3a59922 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngineTest.java @@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.enricher.matching; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.model.Event; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.util.List; @@ -337,4 +338,27 @@ class StrictFqnMatchingEngineTest { assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); } + + @Test + void shouldNotMatchImportStyleEnumWhenSimpleNameIsAmbiguous(@TempDir java.nio.file.Path tempDir) throws Exception { + java.nio.file.Files.createDirectories(tempDir.resolve("a")); + java.nio.file.Files.createDirectories(tempDir.resolve("b")); + java.nio.file.Files.writeString(tempDir.resolve("a/OrderEvent.java"), + "package a; public enum OrderEvent { PAY }"); + java.nio.file.Files.writeString(tempDir.resolve("b/OrderEvent.java"), + "package b; public enum OrderEvent { PAY }"); + + click.kamil.springstatemachineexporter.ast.common.CodebaseContext context = + new click.kamil.springstatemachineexporter.ast.common.CodebaseContext(); + context.scan(tempDir); + + StrictFqnMatchingEngine engine = new StrictFqnMatchingEngine(context); + Event smEvent = Event.of("PAY", "a.OrderEvent.PAY"); + TriggerPoint triggerPoint = TriggerPoint.builder() + .event("OrderEvent.PAY") + .polymorphicEvents(List.of("OrderEvent.PAY")) + .build(); + + assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); + } }