Compare commits
2 Commits
d19d4f20ef
...
6ca8e64750
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ca8e64750 | |||
| 5b0778301a |
@@ -49,7 +49,8 @@ public final class CallChainLinkPolicy {
|
||||
&& !isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context);
|
||||
}
|
||||
return MachineEnumCanonicalizer.isDynamicTriggerExpression(event)
|
||||
&& event != null;
|
||||
&& event != null
|
||||
&& !isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,9 +88,7 @@ public final class CallChainLinkPolicy {
|
||||
if (pe == null || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:") || !pe.contains(".")) {
|
||||
return false;
|
||||
}
|
||||
String enumType = pe.substring(0, pe.lastIndexOf('.'));
|
||||
if (!enumType.contains(".")
|
||||
|| !MachineEnumCanonicalizer.enumTypesMatch(eventTypeFqn, enumType, context)) {
|
||||
if (!MachineEnumCanonicalizer.polymorphicEventMatchesMachineEventType(pe, eventTypeFqn, context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -121,7 +120,6 @@ public final class CallChainLinkPolicy {
|
||||
return LinkResolution.UNRESOLVED_EXTERNAL;
|
||||
}
|
||||
if (ambiguousSource
|
||||
|| (trigger != null && trigger.isAmbiguous())
|
||||
|| shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
|
||||
return LinkResolution.AMBIGUOUS_WIDEN;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import click.kamil.springstatemachineexporter.analysis.enricher.routing.Heuristi
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -57,14 +59,48 @@ public final class MachineScopeFilter {
|
||||
return chains == null ? List.of() : chains;
|
||||
}
|
||||
List<CallChain> filtered = new ArrayList<>();
|
||||
String machineEventTypeFqn = resolveMachineEventTypeFqn(machineName, context, machineTransitions);
|
||||
for (CallChain chain : chains) {
|
||||
if (ROUTING.hasProvenMachineAffinity(chain, machineName, context, machineTransitions)) {
|
||||
filtered.add(chain);
|
||||
continue;
|
||||
}
|
||||
TriggerPoint trigger = chain.getTriggerPoint();
|
||||
if (trigger != null
|
||||
&& machineEventTypeFqn != null
|
||||
&& CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context)) {
|
||||
filtered.add(chain);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static String resolveMachineEventTypeFqn(
|
||||
String machineName,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
if (context != null && machineName != null) {
|
||||
String[] types = StateMachineTypeResolver.resolve(machineName, context);
|
||||
if (types != null && types.length > 1 && types[1] != null && !types[1].isBlank()) {
|
||||
return types[1];
|
||||
}
|
||||
}
|
||||
if (machineTransitions == null) {
|
||||
return null;
|
||||
}
|
||||
for (Transition transition : machineTransitions) {
|
||||
Event event = transition.getEvent();
|
||||
if (event == null) {
|
||||
continue;
|
||||
}
|
||||
String fullIdentifier = event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
||||
if (fullIdentifier != null && fullIdentifier.contains(".")) {
|
||||
return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<EntryPoint> filterEntryPointsForMachine(
|
||||
List<EntryPoint> entryPoints, String machineName, CodebaseContext context) {
|
||||
if (entryPoints == null || machineName == null) {
|
||||
|
||||
@@ -63,6 +63,16 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
tp, machineTypes, context, stateMachineTransitions, true);
|
||||
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, machineTypes, context);
|
||||
tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
|
||||
if (machineTypes != null) {
|
||||
tp = tp.toBuilder()
|
||||
.eventTypeFqn(tp.getEventTypeFqn() != null
|
||||
? tp.getEventTypeFqn()
|
||||
: machineTypes.eventTypeFqn())
|
||||
.stateTypeFqn(tp.getStateTypeFqn() != null
|
||||
? tp.getStateTypeFqn()
|
||||
: machineTypes.stateTypeFqn())
|
||||
.build();
|
||||
}
|
||||
|
||||
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
|
||||
updatedChains.add(chain);
|
||||
@@ -89,7 +99,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
Map<String, String> canonicalSourceBySimplified = new LinkedHashMap<>();
|
||||
for (Transition t : stateMachineTransitions) {
|
||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)
|
||||
&& isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions)
|
||||
&& isRoutedToCorrectMachine(
|
||||
chain, result.getName(), context, stateMachineTransitions, tp,
|
||||
machineTypes != null ? machineTypes.eventTypeFqn() : null)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
String smEventForLink = canonicalEvent(t.getEvent());
|
||||
for (State smSourceState : t.getSourceStates()) {
|
||||
@@ -131,8 +143,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
.targetState(smSourceForLink)
|
||||
.event(smEventForLink)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
if (isRoutedToCorrectMachine(
|
||||
chain, result.getName(), context, stateMachineTransitions, tp,
|
||||
machineTypes != null ? machineTypes.eventTypeFqn() : null)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
} else {
|
||||
@@ -143,7 +157,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
.targetState(targetForLink)
|
||||
.event(smEventForLink)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions)
|
||||
if (isRoutedToCorrectMachine(
|
||||
chain, result.getName(), context, stateMachineTransitions, tp,
|
||||
machineTypes != null ? machineTypes.eventTypeFqn() : null)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
@@ -201,10 +217,26 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
|
||||
private boolean isRoutedToCorrectMachine(
|
||||
CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) {
|
||||
CallChain chain,
|
||||
String currentMachineName,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions,
|
||||
TriggerPoint triggerPoint,
|
||||
String machineEventTypeFqn) {
|
||||
if (triggerPoint != null
|
||||
&& machineEventTypeFqn != null
|
||||
&& CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(triggerPoint, machineEventTypeFqn, context)) {
|
||||
return true;
|
||||
}
|
||||
return routingEngine.hasProvenMachineAffinity(chain, currentMachineName, context, machineTransitions);
|
||||
}
|
||||
|
||||
private boolean isRoutedToCorrectMachine(
|
||||
CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) {
|
||||
return isRoutedToCorrectMachine(
|
||||
chain, currentMachineName, context, machineTransitions, chain.getTriggerPoint(), null);
|
||||
}
|
||||
|
||||
private boolean isConstraintCompatible(String constraint, String machineName) {
|
||||
if (constraint == null || machineName == null) {
|
||||
return true;
|
||||
|
||||
@@ -131,7 +131,15 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
}
|
||||
|
||||
private boolean typesMatch(String type1, String type2) {
|
||||
return MachineEnumCanonicalizer.enumTypesMatch(type1, type2, context);
|
||||
if (MachineEnumCanonicalizer.enumTypesMatch(type1, type2, context)) {
|
||||
return true;
|
||||
}
|
||||
if (type1 != null && type2 != null) {
|
||||
String machineFqn = type1.contains(".") ? type1 : type2;
|
||||
String candidate = type1.contains(".") ? type2 : type1;
|
||||
return MachineEnumCanonicalizer.enumTypesMatchForMachine(machineFqn, candidate, context);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String constantName(String event) {
|
||||
|
||||
@@ -126,8 +126,13 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
if (mismatched) {
|
||||
return false;
|
||||
}
|
||||
// Provable generic types but no machine match — fail closed (no package-name fallback).
|
||||
return false;
|
||||
// Fail closed only when both trigger and machine generic types were resolved but
|
||||
// could not be reconciled. When machine config AST does not expose type args (stub
|
||||
// config classes in tests, generated configs), fall through to single-SM / package routing.
|
||||
boolean machineTypesKnown = machineEventFqn != null || machineStateFqn != null;
|
||||
if (machineTypesKnown) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -842,18 +842,17 @@ public final class MachineEnumCanonicalizer {
|
||||
String constant = constantName(stripped);
|
||||
String typePart = enumTypeFromRef(stripped);
|
||||
|
||||
if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
|
||||
return stripped;
|
||||
if (typePart != null && !typePart.contains(".") && importStyleEnumTypeMatches(typePart, enumTypeFqn)
|
||||
&& constant.matches("[A-Z_][A-Z0-9_]*")) {
|
||||
return enumTypeFqn + "." + constant;
|
||||
}
|
||||
|
||||
if (typePart != null && enumTypesMatch(enumTypeFqn, typePart, context)) {
|
||||
return enumTypeFqn + "." + constant;
|
||||
}
|
||||
|
||||
if (typePart != null && !typePart.contains(".") && !enumTypesMatch(enumTypeFqn, typePart, context)
|
||||
&& importStyleEnumTypeMatches(typePart, enumTypeFqn)
|
||||
&& constant.matches("[A-Z_][A-Z0-9_]*")) {
|
||||
return enumTypeFqn + "." + constant;
|
||||
if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
|
||||
return stripped;
|
||||
}
|
||||
|
||||
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
|
||||
@@ -917,6 +916,47 @@ public final class MachineEnumCanonicalizer {
|
||||
return enumTypesMatch(type1, type2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a concrete polymorphic enum constant belongs to the configured machine event type.
|
||||
* Uses machine context to accept import-style labels even when the enum simple name is
|
||||
* ambiguous across the scanned codebase.
|
||||
*/
|
||||
public static boolean polymorphicEventMatchesMachineEventType(
|
||||
String polymorphicEvent,
|
||||
String machineEventTypeFqn,
|
||||
CodebaseContext context) {
|
||||
if (polymorphicEvent == null || machineEventTypeFqn == null || !polymorphicEvent.contains(".")) {
|
||||
return false;
|
||||
}
|
||||
if (polymorphicEvent.startsWith("<SYMBOLIC:") || polymorphicEvent.startsWith("ENUM_SET:")) {
|
||||
return false;
|
||||
}
|
||||
String enumType = polymorphicEvent.substring(0, polymorphicEvent.lastIndexOf('.'));
|
||||
if (machineEventTypeFqn.equals(enumType)) {
|
||||
return true;
|
||||
}
|
||||
if (importStyleEnumTypeMatches(enumType, machineEventTypeFqn)) {
|
||||
return true;
|
||||
}
|
||||
return enumType.contains(".") && enumTypesMatch(machineEventTypeFqn, enumType, context);
|
||||
}
|
||||
|
||||
public static boolean enumTypesMatchForMachine(
|
||||
String machineEnumFqn,
|
||||
String candidateType,
|
||||
CodebaseContext context) {
|
||||
if (machineEnumFqn == null || candidateType == null) {
|
||||
return false;
|
||||
}
|
||||
if (machineEnumFqn.equals(candidateType)) {
|
||||
return true;
|
||||
}
|
||||
if (importStyleEnumTypeMatches(candidateType, machineEnumFqn)) {
|
||||
return true;
|
||||
}
|
||||
return candidateType.contains(".") && enumTypesMatch(machineEnumFqn, candidateType, context);
|
||||
}
|
||||
|
||||
private static String simpleName(String fqn) {
|
||||
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
|
||||
}
|
||||
|
||||
@@ -672,9 +672,7 @@ public final class EntryPointBindingExpander {
|
||||
org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding();
|
||||
if (fieldBinding != null && fieldBinding.isField()) {
|
||||
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = fieldBinding.getDeclaringClass();
|
||||
if (declaringClass != null) {
|
||||
declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName());
|
||||
}
|
||||
declaringType = resolveFieldDeclaringType(declaringClass, compilationUnit, context);
|
||||
}
|
||||
if (declaringType == null) {
|
||||
Expression qualifier = fieldAccess.getExpression();
|
||||
@@ -693,9 +691,7 @@ public final class EntryPointBindingExpander {
|
||||
if (binding instanceof org.eclipse.jdt.core.dom.IVariableBinding variableBinding
|
||||
&& variableBinding.isField()) {
|
||||
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = variableBinding.getDeclaringClass();
|
||||
if (declaringClass != null) {
|
||||
declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName());
|
||||
}
|
||||
declaringType = resolveFieldDeclaringType(declaringClass, compilationUnit, context);
|
||||
}
|
||||
if (declaringType == null && compilationUnit != null) {
|
||||
declaringType = context.resolveStaticImport(fieldName, compilationUnit);
|
||||
@@ -724,6 +720,27 @@ public final class EntryPointBindingExpander {
|
||||
return extractStaticFieldMapInitializer(declaringType, fieldName, context);
|
||||
}
|
||||
|
||||
private static TypeDeclaration resolveFieldDeclaringType(
|
||||
org.eclipse.jdt.core.dom.ITypeBinding declaringClass,
|
||||
CompilationUnit compilationUnit,
|
||||
CodebaseContext context) {
|
||||
if (declaringClass == null) {
|
||||
return null;
|
||||
}
|
||||
org.eclipse.jdt.core.dom.ITypeBinding erased = declaringClass.getErasure();
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(erased.getQualifiedName());
|
||||
if (typeDeclaration != null) {
|
||||
return typeDeclaration;
|
||||
}
|
||||
if (compilationUnit != null) {
|
||||
typeDeclaration = context.getTypeDeclaration(erased.getName(), compilationUnit);
|
||||
if (typeDeclaration != null) {
|
||||
return typeDeclaration;
|
||||
}
|
||||
}
|
||||
return context.getTypeDeclaration(erased.getName());
|
||||
}
|
||||
|
||||
private static boolean isStaticMapFieldReceiver(Expression receiver) {
|
||||
if (receiver instanceof FieldAccess fieldAccess) {
|
||||
org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding();
|
||||
|
||||
@@ -102,4 +102,38 @@ class CallChainLinkPolicyTest {
|
||||
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger)).isFalse();
|
||||
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(trigger)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTrustImportStylePolymorphicWidenWhenMachineEventTypeIsKnown() {
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.ambiguous(true)
|
||||
.event("OrderEvent.valueOf(eventStr)")
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
|
||||
.build();
|
||||
|
||||
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(
|
||||
trigger, "com.example.order.OrderEvent")).isTrue();
|
||||
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
||||
trigger, "com.example.order.OrderEvent")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveWhenTrustedWidenHasMatchesDespiteAmbiguousCallGraphFlag() {
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.ambiguous(true)
|
||||
.event("OrderEvent.valueOf(eventStr)")
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
|
||||
.build();
|
||||
|
||||
var matched = List.of(
|
||||
click.kamil.springstatemachineexporter.analysis.model.MatchedTransition.builder()
|
||||
.event("com.example.order.OrderEvent.PAY")
|
||||
.build());
|
||||
|
||||
assertThat(CallChainLinkPolicy.resolveLinkResolution(
|
||||
trigger,
|
||||
matched,
|
||||
false,
|
||||
"com.example.order.OrderEvent")).isEqualTo(LinkResolution.RESOLVED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression for enterprise multi-module scans: a globally ambiguous enum simple name must not
|
||||
* block machine-scoped {@code valueOf} widens from linking when polymorphic events use import-style
|
||||
* labels and the machine event type is known.
|
||||
*/
|
||||
class TrustedEnumWidenLinkingRegressionTest {
|
||||
|
||||
@Test
|
||||
void shouldLinkTrustedImportStyleWidenWhenEnumSimpleNameIsGloballyAmbiguous(@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);
|
||||
assertThat(context.isAmbiguousSimpleName("OrderEvent")).isTrue();
|
||||
|
||||
Transition pay = new Transition();
|
||||
pay.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
|
||||
pay.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
||||
pay.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));
|
||||
|
||||
Transition ship = new Transition();
|
||||
ship.setSourceStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
||||
ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.order.OrderState.SHIPPED")));
|
||||
ship.setEvent(Event.of("SHIP", "com.example.order.OrderEvent.SHIP"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("OrderEvent.valueOf(eventStr)")
|
||||
.ambiguous(true)
|
||||
.external(false)
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.transitions(List.of(pay, ship))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
new TransitionLinkerEnricher().enrich(result, null, null);
|
||||
|
||||
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||
assertThat(linked.getMatchedTransitions()).hasSizeGreaterThanOrEqualTo(1);
|
||||
assertThat(linked.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepTrustedWidenCallChainWhenEnumSimpleNameIsGloballyAmbiguous(@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);
|
||||
assertThat(context.isAmbiguousSimpleName("OrderEvent")).isTrue();
|
||||
|
||||
Transition pay = new Transition();
|
||||
pay.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
|
||||
pay.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
||||
pay.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));
|
||||
|
||||
Transition ship = new Transition();
|
||||
ship.setSourceStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
||||
ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.order.OrderState.SHIPPED")));
|
||||
ship.setEvent(Event.of("SHIP", "com.example.order.OrderEvent.SHIP"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("OrderEvent.valueOf(eventStr)")
|
||||
.ambiguous(true)
|
||||
.external(false)
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
List<CallChain> scoped = MachineScopeFilter.filterCallChainsForMachine(
|
||||
List.of(chain),
|
||||
"com.example.config.OrderStateMachineConfiguration",
|
||||
context,
|
||||
List.of(pay, ship));
|
||||
|
||||
assertThat(scoped).hasSize(1);
|
||||
}
|
||||
}
|
||||
@@ -340,7 +340,7 @@ class StrictFqnMatchingEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchImportStyleEnumWhenSimpleNameIsAmbiguous(@TempDir java.nio.file.Path tempDir) throws Exception {
|
||||
void shouldMatchImportStyleEnumToMachineFqnWhenSimpleNamesAlign(@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"),
|
||||
@@ -359,6 +359,6 @@ class StrictFqnMatchingEngineTest {
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRewriteImportStyleWhenSimpleNameIsAmbiguous(@TempDir Path tempDir) throws Exception {
|
||||
void shouldCanonicalizeImportStyleWhenMachineEnumTypeIsKnown(@TempDir Path tempDir) throws Exception {
|
||||
Files.createDirectories(tempDir.resolve("a"));
|
||||
Files.createDirectories(tempDir.resolve("b"));
|
||||
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
|
||||
@@ -44,7 +44,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
|
||||
context.scan(tempDir);
|
||||
|
||||
assertThat(MachineEnumCanonicalizer.canonicalizeLabel("OrderEvent.PAY", "a.OrderEvent", context))
|
||||
.isEqualTo("OrderEvent.PAY");
|
||||
.isEqualTo("a.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -112,7 +112,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCanonicalizeAmbiguousImportStyleTransitionEvents(@TempDir Path tempDir) throws Exception {
|
||||
void shouldCanonicalizeAmbiguousImportStyleTransitionEventsWhenMachineTypeKnown(@TempDir Path tempDir) throws Exception {
|
||||
Files.createDirectories(tempDir.resolve("a"));
|
||||
Files.createDirectories(tempDir.resolve("b"));
|
||||
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
|
||||
@@ -138,7 +138,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
|
||||
"a.OrderState", "a.OrderEvent");
|
||||
MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types, context);
|
||||
|
||||
assertThat(transition.getEvent().fullIdentifier()).isEqualTo("OrderEvent.PAY");
|
||||
assertThat(transition.getEvent().fullIdentifier()).isEqualTo("a.OrderEvent.PAY");
|
||||
assertThat(transition.getSourceStates().get(0).fullIdentifier()).isEqualTo("a.OrderState.NEW");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ class AmbiguousSimpleEnumLinkingTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRewriteAmbiguousImportStylePolyDuringTriggerCanonicalization(@TempDir Path tempDir) throws Exception {
|
||||
void shouldCanonicalizeImportStylePolyWhenMachineEventTypeIsKnown(@TempDir Path tempDir) throws Exception {
|
||||
Files.createDirectories(tempDir.resolve("a"));
|
||||
Files.createDirectories(tempDir.resolve("b"));
|
||||
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
|
||||
@@ -257,7 +257,7 @@ class AmbiguousSimpleEnumLinkingTest {
|
||||
|
||||
TriggerPoint canonical = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(canonical.getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY", "OrderEvent.SHIP");
|
||||
.containsExactly("a.OrderEvent.PAY", "a.OrderEvent.SHIP");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ class AnalysisResultFinalizerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveAmbiguousImportStyleMatchedTransitions(@TempDir Path tempDir) throws Exception {
|
||||
void shouldCanonicalizeAmbiguousImportStyleMatchedTransitions(@TempDir Path tempDir) throws Exception {
|
||||
Files.createDirectories(tempDir.resolve("a"));
|
||||
Files.createDirectories(tempDir.resolve("b"));
|
||||
Files.createDirectories(tempDir.resolve("com/example/config"));
|
||||
@@ -165,7 +165,7 @@ class AnalysisResultFinalizerTest {
|
||||
"a.OrderState", "a.OrderEvent"));
|
||||
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||
.isEqualTo("OrderEvent.PAY");
|
||||
.isEqualTo("a.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
private static void writeSampleConfig(Path tempDir) throws Exception {
|
||||
|
||||
@@ -883,4 +883,227 @@ class MultiModulePathBindingExpanderTest {
|
||||
assertThat(variants).extracting(map -> map.get("commandKey"))
|
||||
.containsExactlyInAnyOrder("order.pay", "order.ship");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExpandCommandKeyFromQualifiedInheritedStaticMapInSiblingModule(@TempDir Path tempDir) throws Exception {
|
||||
Path root = tempDir.resolve("order-system");
|
||||
Files.createDirectories(root);
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");
|
||||
|
||||
Path apiModule = root.resolve("api-module");
|
||||
Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
|
||||
Files.writeString(apiModule.resolve("build.gradle"), "");
|
||||
Files.writeString(apiModule.resolve("src/main/java/com/example/routes/BaseRoutes.java"), """
|
||||
package com.example.routes;
|
||||
import java.util.Map;
|
||||
public abstract class BaseRoutes {
|
||||
protected static final Map<String, String> ROUTES = Map.of(
|
||||
"order.pay", "OrderEvent.PAY",
|
||||
"order.ship", "OrderEvent.SHIP");
|
||||
}
|
||||
""");
|
||||
|
||||
Path coreModule = root.resolve("core-module");
|
||||
Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
|
||||
Files.writeString(coreModule.resolve("build.gradle"),
|
||||
"dependencies { implementation project(':api-module') }");
|
||||
Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
|
||||
package com.example;
|
||||
import com.example.routes.BaseRoutes;
|
||||
public class GenericCommandController {
|
||||
OrderGateway gateway;
|
||||
public void execute(String commandKey) {
|
||||
gateway.trigger(commandKey);
|
||||
}
|
||||
}
|
||||
class OrderGateway extends BaseRoutes {
|
||||
void trigger(String commandKey) {
|
||||
OrderEvent event = OrderEvent.valueOf(BaseRoutes.ROUTES.get(commandKey));
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(event);
|
||||
}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
""");
|
||||
|
||||
SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
|
||||
ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
|
||||
Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.scan(scanPaths, Collections.emptySet());
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
|
||||
engine.buildCallGraph();
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.GenericCommandController")
|
||||
.methodName("execute")
|
||||
.name("POST /api/commands/{commandKey}")
|
||||
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||
.name("commandKey")
|
||||
.type("String")
|
||||
.annotations(List.of("PathVariable"))
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
List<Map<String, String>> variants =
|
||||
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);
|
||||
|
||||
assertThat(variants).extracting(map -> map.get("commandKey"))
|
||||
.containsExactlyInAnyOrder("order.pay", "order.ship");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExpandCommandKeyFromStaticMethodMapReceiverInSiblingModule(@TempDir Path tempDir) throws Exception {
|
||||
Path root = tempDir.resolve("order-system");
|
||||
Files.createDirectories(root);
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");
|
||||
|
||||
Path apiModule = root.resolve("api-module");
|
||||
Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
|
||||
Files.writeString(apiModule.resolve("build.gradle"), "");
|
||||
Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
|
||||
package com.example.routes;
|
||||
import java.util.Map;
|
||||
public class CommandRoutes {
|
||||
public static Map<String, String> routes() {
|
||||
return Map.of(
|
||||
"order.pay", "OrderEvent.PAY",
|
||||
"order.ship", "OrderEvent.SHIP");
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Path coreModule = root.resolve("core-module");
|
||||
Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
|
||||
Files.writeString(coreModule.resolve("build.gradle"),
|
||||
"dependencies { implementation project(':api-module') }");
|
||||
Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
|
||||
package com.example;
|
||||
import com.example.routes.CommandRoutes;
|
||||
public class GenericCommandController {
|
||||
OrderGateway gateway;
|
||||
public void execute(String commandKey) {
|
||||
gateway.trigger(commandKey);
|
||||
}
|
||||
}
|
||||
class OrderGateway {
|
||||
void trigger(String commandKey) {
|
||||
OrderEvent event = OrderEvent.valueOf(CommandRoutes.routes().get(commandKey));
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(event);
|
||||
}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
""");
|
||||
|
||||
SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
|
||||
ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
|
||||
Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.scan(scanPaths, Collections.emptySet());
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
|
||||
engine.buildCallGraph();
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.GenericCommandController")
|
||||
.methodName("execute")
|
||||
.name("POST /api/commands/{commandKey}")
|
||||
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||
.name("commandKey")
|
||||
.type("String")
|
||||
.annotations(List.of("PathVariable"))
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
List<Map<String, String>> variants =
|
||||
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);
|
||||
|
||||
assertThat(variants).extracting(map -> map.get("commandKey"))
|
||||
.containsExactlyInAnyOrder("order.pay", "order.ship");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExpandRequestParamFromStaticMapLookupInSiblingModule(@TempDir Path tempDir) throws Exception {
|
||||
Path root = tempDir.resolve("order-system");
|
||||
Files.createDirectories(root);
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");
|
||||
|
||||
Path apiModule = root.resolve("api-module");
|
||||
Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
|
||||
Files.writeString(apiModule.resolve("build.gradle"), "");
|
||||
Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
|
||||
package com.example.routes;
|
||||
import java.util.Map;
|
||||
public class CommandRoutes {
|
||||
public static final Map<String, String> ROUTES = Map.of(
|
||||
"order.pay", "OrderEvent.PAY",
|
||||
"order.ship", "OrderEvent.SHIP");
|
||||
}
|
||||
""");
|
||||
|
||||
Path coreModule = root.resolve("core-module");
|
||||
Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
|
||||
Files.writeString(coreModule.resolve("build.gradle"),
|
||||
"dependencies { implementation project(':api-module') }");
|
||||
Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
|
||||
package com.example;
|
||||
import com.example.routes.CommandRoutes;
|
||||
public class GenericCommandController {
|
||||
OrderGateway gateway;
|
||||
public void execute(String commandKey) {
|
||||
gateway.trigger(commandKey);
|
||||
}
|
||||
}
|
||||
class OrderGateway {
|
||||
void trigger(String commandKey) {
|
||||
OrderEvent event = OrderEvent.valueOf(CommandRoutes.ROUTES.get(commandKey));
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(event);
|
||||
}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
""");
|
||||
|
||||
SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
|
||||
ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
|
||||
Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.scan(scanPaths, Collections.emptySet());
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
|
||||
engine.buildCallGraph();
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.GenericCommandController")
|
||||
.methodName("execute")
|
||||
.name("POST /api/commands")
|
||||
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||
.name("commandKey")
|
||||
.type("String")
|
||||
.annotations(List.of("RequestParam"))
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
List<Map<String, String>> variants =
|
||||
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);
|
||||
|
||||
assertThat(variants).extracting(map -> map.get("commandKey"))
|
||||
.containsExactlyInAnyOrder("order.pay", "order.ship");
|
||||
assertThat(EntryPointBindingExpander.withResolvedPath(entryPoint, variants.get(0)).getName())
|
||||
.isEqualTo("POST /api/commands?commandKey=order.pay");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +291,28 @@ class CodebaseContextTest {
|
||||
assertThat(context.areClassesPolymorphicallyCompatible("a.Dispatcher", "b.Dispatcher")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSuperclassFqnShouldPreferImportAwareResolutionWhenBindingMisResolvesPackage() throws IOException {
|
||||
Files.createDirectories(tempDir.resolve("com/example/routes"));
|
||||
Files.writeString(tempDir.resolve("com/example/routes/BaseRoutes.java"), """
|
||||
package com.example.routes;
|
||||
public abstract class BaseRoutes {}
|
||||
""");
|
||||
Files.writeString(tempDir.resolve("com/example/App.java"), """
|
||||
package com.example;
|
||||
import com.example.routes.BaseRoutes;
|
||||
class OrderGateway extends BaseRoutes {}
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.scan(tempDir);
|
||||
|
||||
assertThat(context.getTypeDeclaration("com.example.routes.BaseRoutes")).isNotNull();
|
||||
assertThat(context.getSuperclassFqn(context.getTypeDeclaration("com.example.OrderGateway")))
|
||||
.isEqualTo("com.example.routes.BaseRoutes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
|
||||
String source = """
|
||||
|
||||
@@ -738,8 +738,16 @@
|
||||
"ambiguous" : true
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null,
|
||||
"linkResolution" : "AMBIGUOUS_WIDEN"
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
|
||||
"targetState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.PAID",
|
||||
"event" : "click.kamil.examples.statemachine.polymorphic.OrderEvents.PAY"
|
||||
}, {
|
||||
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.PAID",
|
||||
"targetState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.CANCELED",
|
||||
"event" : "click.kamil.examples.statemachine.polymorphic.OrderEvents.ABCD"
|
||||
} ],
|
||||
"linkResolution" : "RESOLVED"
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : {
|
||||
|
||||
Reference in New Issue
Block a user