Avoid source-state inference collisions across enum types
When inferring missing trigger sourceState from the transition table, preserve enum type context (Type.CONST) so different state enums sharing a constant name do not collapse into a single inferred source. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -65,7 +65,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;
|
String triggerSource = tp.getSourceState() != null ? simplifySourceState(tp.getSourceState()) : null;
|
||||||
if (CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(tp)) {
|
if (CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(tp)) {
|
||||||
updatedChains.add(chain.toBuilder()
|
updatedChains.add(chain.toBuilder()
|
||||||
.triggerPoint(tp)
|
.triggerPoint(tp)
|
||||||
@@ -87,7 +87,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
String smEventForLink = canonicalEvent(t.getEvent());
|
String smEventForLink = canonicalEvent(t.getEvent());
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
String smSourceForLink = canonicalState(smSourceState);
|
String smSourceForLink = canonicalState(smSourceState);
|
||||||
String smSource = simplify(smSourceForLink);
|
String smSource = simplifySourceState(smSourceForLink);
|
||||||
sourcesByEvent.computeIfAbsent(smEventForLink, ignored -> new LinkedHashSet<>())
|
sourcesByEvent.computeIfAbsent(smEventForLink, ignored -> new LinkedHashSet<>())
|
||||||
.add(smSource);
|
.add(smSource);
|
||||||
canonicalSourceBySimplified.putIfAbsent(smSource, smSourceForLink);
|
canonicalSourceBySimplified.putIfAbsent(smSource, smSourceForLink);
|
||||||
@@ -116,7 +116,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
String smEventForLink = canonicalEvent(t.getEvent());
|
String smEventForLink = canonicalEvent(t.getEvent());
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
String smSourceForLink = canonicalState(smSourceState);
|
String smSourceForLink = canonicalState(smSourceState);
|
||||||
String smSource = simplify(smSourceForLink);
|
String smSource = simplifySourceState(smSourceForLink);
|
||||||
if (triggerSource == null || triggerSource.equals(smSource)) {
|
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
@@ -201,23 +201,25 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
constraint, MachineDomainKeys.extractMachineDomainKey(machineName));
|
constraint, MachineDomainKeys.extractMachineDomainKey(machineName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
|
private final java.util.Map<String, String> simplifySourceCache = new java.util.HashMap<>();
|
||||||
private static final java.util.regex.Pattern SUFFIX_PATTERN = java.util.regex.Pattern.compile("(?i)(.+)(Event|Action|Transition|Command)(s)?$");
|
|
||||||
private static final java.util.regex.Pattern FQN_PATTERN = java.util.regex.Pattern.compile("^.*\\.([A-Z0-9_]+)$");
|
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String simplifySourceState(String name) {
|
||||||
if (name == null) return null;
|
if (name == null) return null;
|
||||||
if (simplifyCache.containsKey(name)) return simplifyCache.get(name);
|
if (simplifySourceCache.containsKey(name)) return simplifySourceCache.get(name);
|
||||||
|
|
||||||
// Strip common suffixes
|
// For source states we must preserve enum type context to avoid collisions between
|
||||||
String simplified = SUFFIX_PATTERN.matcher(name).replaceAll("$1");
|
// different state enums that share a constant name (e.g. OrderState.NEW vs InvoiceState.NEW).
|
||||||
if (simplified.isEmpty()) {
|
// Use EnumFormat.fn-like suffix (Type.CONST) when possible.
|
||||||
simplified = name;
|
String simplified = name;
|
||||||
|
int lastDot = name.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
int prevDot = name.lastIndexOf('.', lastDot - 1);
|
||||||
|
if (prevDot > 0) {
|
||||||
|
simplified = name.substring(prevDot + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Simplify full identifiers to just the last part (enum name)
|
|
||||||
simplified = FQN_PATTERN.matcher(simplified).replaceAll("$1");
|
|
||||||
|
|
||||||
simplifyCache.put(name, simplified);
|
simplifySourceCache.put(name, simplified);
|
||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,36 @@ class TransitionLinkerEnricherTest {
|
|||||||
assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isFalse();
|
assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotInferSourceStateWhenDifferentEnumsShareSameConstantName() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
||||||
|
t1.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
Transition t2 = new Transition();
|
||||||
|
t2.setSourceStates(List.of(State.of("NEW", "com.example.invoice.InvoiceState.NEW")));
|
||||||
|
t2.setTargetStates(List.of(State.of("PAID", "com.example.invoice.InvoiceState.PAID")));
|
||||||
|
t2.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("PAY").build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.transitions(List.of(t1, t2))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChain.getTriggerPoint().getSourceState()).isNull();
|
||||||
|
assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isTrue();
|
||||||
|
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
|
||||||
|
assertThat(updatedChain.getLinkResolution()).isEqualTo(click.kamil.springstatemachineexporter.analysis.model.LinkResolution.AMBIGUOUS_WIDEN);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldLinkTransitionByEventAndSourceState() {
|
void shouldLinkTransitionByEventAndSourceState() {
|
||||||
Transition t1 = new Transition();
|
Transition t1 = new Transition();
|
||||||
|
|||||||
Reference in New Issue
Block a user