Enforce package-canonical enum identifiers across analysis export.
Resolve generic configurer type arguments, canonicalize transitions and triggers consistently, validate after property resolution, and update regression goldens. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Final enrichment step: fail fast when enum identifiers in the analysis model are not
|
||||
* package-canonical. Disable with {@code -Danalyzer.canonical-form-validation.enabled=false}.
|
||||
*/
|
||||
@Slf4j
|
||||
public class AnalysisCanonicalFormEnricher implements AnalysisEnricher {
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
public AnalysisCanonicalFormEnricher() {
|
||||
this.enabled = Boolean.parseBoolean(
|
||||
System.getProperty("analyzer.canonical-form-validation.enabled", "true"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
if (!enabled || result == null || context == null) {
|
||||
return;
|
||||
}
|
||||
log.debug("Validating canonical enum identifiers for {}", result.getName());
|
||||
AnalysisCanonicalFormValidator.enforce(result, context);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,15 @@ 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.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class CallChainEnricher implements AnalysisEnricher {
|
||||
@@ -15,13 +19,28 @@ public class CallChainEnricher implements AnalysisEnricher {
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
log.info("Enriching {} with call chains", result.getName());
|
||||
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
|
||||
List<TriggerPoint> canonicalTriggers = intelligence.findTriggerPoints().stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<CallChain> chains = intelligence.findCallChains(
|
||||
result.getMetadata().getEntryPoints(),
|
||||
intelligence.findTriggerPoints()
|
||||
canonicalTriggers
|
||||
);
|
||||
chains = chains.stream()
|
||||
.map(chain -> chain.getTriggerPoint() == null
|
||||
? chain
|
||||
: chain.toBuilder()
|
||||
.triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
||||
chain.getTriggerPoint(), machineTypes))
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||
|
||||
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.callChains(chains)
|
||||
.build());
|
||||
|
||||
@@ -15,17 +15,14 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||
|
||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
private static final ExportOptions LINK_FORMAT_OPTIONS = ExportOptions.builder().build();
|
||||
|
||||
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
||||
private final EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine();
|
||||
|
||||
@@ -52,9 +49,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
for (Transition t : stateMachineTransitions) {
|
||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
||||
String smEventForLink = formatEventForLink(t.getEvent());
|
||||
String smEventForLink = canonicalEvent(t.getEvent());
|
||||
for (State smSourceState : t.getSourceStates()) {
|
||||
String smSourceForLink = formatSourceForLink(smSourceState);
|
||||
String smSourceForLink = canonicalState(smSourceState);
|
||||
String smSource = simplify(smSourceForLink);
|
||||
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||
@@ -69,7 +66,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
} else {
|
||||
for (State smTargetState : t.getTargetStates()) {
|
||||
String targetForLink = formatSourceForLink(smTargetState);
|
||||
String targetForLink = canonicalState(smTargetState);
|
||||
MatchedTransition mt = MatchedTransition.builder()
|
||||
.sourceState(smSourceForLink)
|
||||
.targetState(targetForLink)
|
||||
@@ -162,20 +159,18 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
return simplified;
|
||||
}
|
||||
|
||||
private String formatSourceForLink(State state) {
|
||||
if (state == null) return null;
|
||||
return simplifyForLink(LINK_FORMAT_OPTIONS.formatState(state));
|
||||
private String canonicalState(State state) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
return state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName();
|
||||
}
|
||||
|
||||
private String formatEventForLink(click.kamil.springstatemachineexporter.model.Event event) {
|
||||
if (event == null) return null;
|
||||
return LINK_FORMAT_OPTIONS.formatEvent(event);
|
||||
}
|
||||
|
||||
private String simplifyForLink(String name) {
|
||||
if (name == null) return "";
|
||||
if (name.contains("\"")) return name;
|
||||
return name.replaceAll("[^a-zA-Z0-9_.]", "");
|
||||
private String canonicalEvent(click.kamil.springstatemachineexporter.model.Event event) {
|
||||
if (event == null) {
|
||||
return null;
|
||||
}
|
||||
return event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
||||
}
|
||||
|
||||
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Canonicalizes trigger-side enum identifiers ({@code event}, {@code polymorphicEvents},
|
||||
* {@code sourceState}) using the owning machine config's {@code <State, Event>} types.
|
||||
*/
|
||||
@Slf4j
|
||||
public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
||||
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
if (result.getMetadata() == null || context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
|
||||
List<TriggerPoint> triggers = result.getMetadata().getTriggers();
|
||||
List<TriggerPoint> canonicalTriggers = triggers == null ? null : triggers.stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<CallChain> callChains = result.getMetadata().getCallChains();
|
||||
List<CallChain> canonicalChains = callChains == null ? null : callChains.stream()
|
||||
.map(chain -> {
|
||||
if (chain.getTriggerPoint() == null) {
|
||||
return chain;
|
||||
}
|
||||
return chain.toBuilder()
|
||||
.triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
||||
chain.getTriggerPoint(), machineTypes))
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (canonicalTriggers == null && canonicalChains == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Canonicalized triggers for {}", result.getName());
|
||||
|
||||
result.setMetadata(CodebaseMetadata.builder()
|
||||
.triggers(canonicalTriggers != null ? canonicalTriggers : triggers)
|
||||
.entryPoints(result.getMetadata().getEntryPoints())
|
||||
.callChains(canonicalChains != null ? canonicalChains : callChains)
|
||||
.properties(result.getMetadata().getProperties())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,14 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class TriggerEnricher implements AnalysisEnricher {
|
||||
@@ -14,8 +17,13 @@ public class TriggerEnricher implements AnalysisEnricher {
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
log.info("Enriching {} with triggers", result.getName());
|
||||
|
||||
List<TriggerPoint> triggers = intelligence.findTriggerPoints();
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
|
||||
List<TriggerPoint> triggers = intelligence.findTriggerPoints().stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList());
|
||||
triggers = MachineScopeFilter.filterTriggersForMachine(triggers, result.getName(), context);
|
||||
|
||||
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
|
||||
@@ -12,18 +12,18 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
return false;
|
||||
}
|
||||
|
||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||
String rawTriggerEvent = triggerPoint.getEvent();
|
||||
|
||||
if (triggerPoint.isExternal()) {
|
||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null
|
||||
? triggerPoint.getPolymorphicEvents()
|
||||
: java.util.Collections.emptyList();
|
||||
if (polyEvents.isEmpty()) {
|
||||
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||
String rawTriggerEvent = triggerPoint.getEvent();
|
||||
|
||||
if (rawTriggerEvent != null && rawTriggerEvent.startsWith("<SYMBOLIC: ") && rawTriggerEvent.endsWith(".*>")) {
|
||||
return false;
|
||||
}
|
||||
@@ -150,9 +150,13 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
}
|
||||
|
||||
if (eventStr.contains(".")) {
|
||||
String constantPart = eventStr.substring(eventStr.lastIndexOf('.') + 1);
|
||||
if (!constantPart.isEmpty() && Character.isUpperCase(constantPart.charAt(0))) {
|
||||
return false;
|
||||
}
|
||||
String firstPart = eventStr.substring(0, eventStr.indexOf('.'));
|
||||
if (!firstPart.isEmpty() && Character.isLowerCase(firstPart.charAt(0))) {
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
@@ -16,17 +17,18 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
||||
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
||||
if (triggerEventFqn != null || triggerStateFqn != null) {
|
||||
String[] machineTypes = getStateMachineTypeArguments(currentMachineName, context);
|
||||
String[] machineTypes = StateMachineTypeResolver.resolve(currentMachineName, context);
|
||||
String machineStateFqn = machineTypes[0];
|
||||
String machineEventFqn = machineTypes[1];
|
||||
|
||||
if (triggerEventFqn != null && machineEventFqn != null) {
|
||||
if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) {
|
||||
return true; // Match!
|
||||
return true;
|
||||
}
|
||||
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||
return false; // Mismatch!
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
||||
@@ -298,59 +300,6 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String[] getStateMachineTypeArguments(String machineName, CodebaseContext context) {
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(machineName);
|
||||
if (td != null) {
|
||||
org.eclipse.jdt.core.dom.Type superclass = td.getSuperclassType();
|
||||
Set<String> visited = new HashSet<>();
|
||||
visited.add(machineName);
|
||||
return findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||
}
|
||||
return new String[]{null, null};
|
||||
}
|
||||
|
||||
private String[] findTypeArgumentsRecursively(org.eclipse.jdt.core.dom.Type type, CodebaseContext context, org.eclipse.jdt.core.dom.CompilationUnit cu, Set<String> visited) {
|
||||
if (type == null) return new String[]{null, null};
|
||||
|
||||
if (type instanceof org.eclipse.jdt.core.dom.ParameterizedType pt) {
|
||||
String rawTypeName = pt.getType().toString();
|
||||
if (rawTypeName.equals("StateMachineConfigurerAdapter") || rawTypeName.equals("EnumStateMachineConfigurerAdapter") ||
|
||||
rawTypeName.endsWith(".StateMachineConfigurerAdapter") || rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
||||
java.util.List<?> typeArgs = pt.typeArguments();
|
||||
if (typeArgs.size() >= 2) {
|
||||
String stateType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(0), cu, context);
|
||||
String eventType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(1), cu, context);
|
||||
return new String[]{stateType, eventType};
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into the raw type declaration's superclass
|
||||
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.add(fqn)) {
|
||||
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
|
||||
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||
if (res[0] != null || res[1] != null) return res;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a simple type (e.g. MyBaseConfig)
|
||||
String simpleName = type.toString();
|
||||
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.add(fqn)) {
|
||||
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
|
||||
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||
if (res[0] != null || res[1] != null) return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{null, null};
|
||||
}
|
||||
|
||||
private String eraseGenerics(String type) {
|
||||
if (type == null) return null;
|
||||
int idx = type.indexOf('<');
|
||||
@@ -360,43 +309,6 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
return type;
|
||||
}
|
||||
|
||||
private String resolveTypeToFqn(org.eclipse.jdt.core.dom.Type type, org.eclipse.jdt.core.dom.CompilationUnit cu, CodebaseContext context) {
|
||||
if (type == null) return null;
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((org.eclipse.jdt.core.dom.SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((org.eclipse.jdt.core.dom.ParameterizedType) type).getType(), cu, context);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
if (simpleName.contains("<")) {
|
||||
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
||||
}
|
||||
|
||||
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
for (Object impObj : cu.imports()) {
|
||||
org.eclipse.jdt.core.dom.ImportDeclaration imp = (org.eclipse.jdt.core.dom.ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
if (!packageName.isEmpty()) {
|
||||
org.eclipse.jdt.core.dom.AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||
if (localTd != null) return context.getFqn(localTd);
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private boolean isTypeMismatched(String type1, String type2) {
|
||||
if (type1 == null || type2 == null) return false;
|
||||
type1 = eraseGenerics(type1);
|
||||
|
||||
@@ -3,6 +3,10 @@ package click.kamil.springstatemachineexporter.analysis.model;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
@@ -72,15 +76,18 @@ public class AnalysisResult {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Resolve metadata (triggers, entry points)
|
||||
// 4. Resolve metadata (triggers, entry points, call chains)
|
||||
if (metadata != null) {
|
||||
if (metadata.getTriggers() != null) {
|
||||
for (var trigger : metadata.getTriggers()) {
|
||||
if (trigger.getEvent() != null) {
|
||||
trigger.setEvent(resolver.resolveValue(trigger.getEvent(), properties));
|
||||
}
|
||||
}
|
||||
}
|
||||
List<TriggerPoint> resolvedTriggers = metadata.getTriggers() == null ? null
|
||||
: metadata.getTriggers().stream()
|
||||
.map(trigger -> resolveTrigger(trigger, resolver, properties))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
List<CallChain> resolvedCallChains = metadata.getCallChains() == null ? null
|
||||
: metadata.getCallChains().stream()
|
||||
.map(chain -> resolveCallChain(chain, resolver, properties))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
if (metadata.getEntryPoints() != null) {
|
||||
for (var ep : metadata.getEntryPoints()) {
|
||||
if (ep.getName() != null) {
|
||||
@@ -95,9 +102,54 @@ public class AnalysisResult {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.metadata = CodebaseMetadata.builder()
|
||||
.triggers(resolvedTriggers != null ? resolvedTriggers : metadata.getTriggers())
|
||||
.entryPoints(metadata.getEntryPoints())
|
||||
.callChains(resolvedCallChains != null ? resolvedCallChains : metadata.getCallChains())
|
||||
.properties(metadata.getProperties())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static TriggerPoint resolveTrigger(
|
||||
TriggerPoint trigger,
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
||||
Map<String, String> properties) {
|
||||
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
||||
: trigger.getPolymorphicEvents().stream()
|
||||
.map(value -> resolver.resolveValue(value, properties))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
return trigger.toBuilder()
|
||||
.event(trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null)
|
||||
.sourceState(trigger.getSourceState() != null
|
||||
? resolver.resolveValue(trigger.getSourceState(), properties)
|
||||
: null)
|
||||
.polymorphicEvents(polymorphicEvents)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static CallChain resolveCallChain(
|
||||
CallChain chain,
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
||||
Map<String, String> properties) {
|
||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
||||
? null
|
||||
: resolveTrigger(chain.getTriggerPoint(), resolver, properties);
|
||||
List<MatchedTransition> matchedTransitions = chain.getMatchedTransitions() == null ? null
|
||||
: chain.getMatchedTransitions().stream()
|
||||
.map(matched -> MatchedTransition.builder()
|
||||
.event(resolver.resolveValue(matched.getEvent(), properties))
|
||||
.sourceState(resolver.resolveValue(matched.getSourceState(), properties))
|
||||
.targetState(resolver.resolveValue(matched.getTargetState(), properties))
|
||||
.build())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
return chain.toBuilder()
|
||||
.triggerPoint(trigger)
|
||||
.matchedTransitions(matchedTransitions)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void addMetadata(CodebaseMetadata newMetadata) {
|
||||
if (newMetadata == null) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.model;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -49,14 +50,14 @@ public class TriggerPoint {
|
||||
this.sourceFile = sourceFile;
|
||||
this.sourceModule = sourceModule;
|
||||
this.stateMachineId = stateMachineId;
|
||||
this.sourceState = sourceState;
|
||||
this.sourceState = MachineEnumCanonicalizer.canonicalizeLabel(sourceState, stateTypeFqn);
|
||||
this.lineNumber = lineNumber;
|
||||
this.stateTypeFqn = stateTypeFqn;
|
||||
this.eventTypeFqn = eventTypeFqn;
|
||||
this.event = qualifyEvent(event, eventTypeFqn);
|
||||
this.event = MachineEnumCanonicalizer.qualifyEventIdentifier(event, eventTypeFqn);
|
||||
if (polymorphicEvents != null) {
|
||||
this.polymorphicEvents = polymorphicEvents.stream()
|
||||
.map(pe -> qualifyEvent(pe, eventTypeFqn))
|
||||
.map(pe -> MachineEnumCanonicalizer.qualifyEventIdentifier(pe, eventTypeFqn))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
} else {
|
||||
this.polymorphicEvents = null;
|
||||
@@ -65,66 +66,4 @@ public class TriggerPoint {
|
||||
this.constraint = constraint;
|
||||
this.ambiguous = ambiguous;
|
||||
}
|
||||
|
||||
private static String qualifyEvent(String e, String eventTypeFqn) {
|
||||
if (e == null || eventTypeFqn == null || e.isEmpty()) {
|
||||
return e;
|
||||
}
|
||||
if (e.startsWith("<SYMBOLIC: ")) {
|
||||
return e;
|
||||
}
|
||||
if (isWildcardVariable(e)) {
|
||||
return e;
|
||||
}
|
||||
if (Character.isLowerCase(e.charAt(0))) {
|
||||
return e;
|
||||
}
|
||||
if (!isEnumConstantCandidate(e)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
String simpleEventType = eventTypeFqn.contains(".") ? eventTypeFqn.substring(eventTypeFqn.lastIndexOf('.') + 1) : eventTypeFqn;
|
||||
|
||||
if (e.contains(".")) {
|
||||
String qualifier = e.substring(0, e.lastIndexOf('.'));
|
||||
String lastSegment = e.substring(e.lastIndexOf('.') + 1);
|
||||
String simpleQualifier = qualifier.contains(".") ? qualifier.substring(qualifier.lastIndexOf('.') + 1) : qualifier;
|
||||
|
||||
if (simpleQualifier.equals(simpleEventType)) {
|
||||
return simpleEventType + "." + lastSegment;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
if (eventTypeFqn.startsWith("java.lang.") || eventTypeFqn.equals("String") || eventTypeFqn.equals("int") || eventTypeFqn.equals("long") || eventTypeFqn.equals("char")) {
|
||||
return e;
|
||||
}
|
||||
return simpleEventType + "." + e;
|
||||
}
|
||||
|
||||
private static boolean isEnumConstantCandidate(String eventStr) {
|
||||
if (eventStr == null) return false;
|
||||
if (eventStr.startsWith("<SYMBOLIC: ")) {
|
||||
return false;
|
||||
}
|
||||
if (eventStr.contains(" ") || eventStr.contains("(") || eventStr.contains(")") || eventStr.contains("?")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isWildcardVariable(String eventStr) {
|
||||
if (eventStr == null) return false;
|
||||
if (eventStr.equals("event") || eventStr.equals("e") ||
|
||||
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||
eventStr.equals("payload")) {
|
||||
return true;
|
||||
}
|
||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
||||
return true;
|
||||
}
|
||||
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Canonicalizes enum-backed state/event identifiers for analysis using the owning machine config's
|
||||
* {@code StateMachineConfigurerAdapter<State, Event>} type parameters.
|
||||
*
|
||||
* <p>Display formatting ({@code --event}/{@code --state}) is separate; this fixes internal
|
||||
* {@code fullIdentifier} values used for matching and traceability.
|
||||
*/
|
||||
public final class MachineEnumCanonicalizer {
|
||||
|
||||
private MachineEnumCanonicalizer() {
|
||||
}
|
||||
|
||||
public static void canonicalizeTransitions(
|
||||
List<Transition> transitions,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
if (transitions == null || machineTypes == null) {
|
||||
return;
|
||||
}
|
||||
for (Transition transition : transitions) {
|
||||
if (transition.getEvent() != null) {
|
||||
transition.setEvent(canonicalizeEvent(transition.getEvent(), machineTypes.eventTypeFqn()));
|
||||
}
|
||||
if (transition.getSourceStates() != null) {
|
||||
transition.setSourceStates(canonicalizeStates(transition.getSourceStates(), machineTypes.stateTypeFqn()));
|
||||
}
|
||||
if (transition.getTargetStates() != null) {
|
||||
transition.setTargetStates(canonicalizeStates(transition.getTargetStates(), machineTypes.stateTypeFqn()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Set<String> canonicalizeStateLabels(Set<String> labels, String stateTypeFqn) {
|
||||
if (labels == null || labels.isEmpty()) {
|
||||
return labels;
|
||||
}
|
||||
return labels.stream()
|
||||
.map(label -> canonicalizeLabel(label, stateTypeFqn))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
public static Set<State> canonicalizeStates(Set<State> states, String stateTypeFqn) {
|
||||
if (states == null || states.isEmpty()) {
|
||||
return states;
|
||||
}
|
||||
return states.stream()
|
||||
.map(state -> canonicalizeState(state, stateTypeFqn))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
public static TriggerPoint canonicalizeTriggerPoint(
|
||||
TriggerPoint trigger,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
if (trigger == null) {
|
||||
return null;
|
||||
}
|
||||
String eventTypeFqn = preferFullTypeFqn(
|
||||
trigger.getEventTypeFqn(),
|
||||
machineTypes != null ? machineTypes.eventTypeFqn() : null);
|
||||
String stateTypeFqn = preferFullTypeFqn(
|
||||
trigger.getStateTypeFqn(),
|
||||
machineTypes != null ? machineTypes.stateTypeFqn() : null);
|
||||
|
||||
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
||||
: trigger.getPolymorphicEvents().stream()
|
||||
.map(event -> canonicalizeLabel(event, eventTypeFqn))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return trigger.toBuilder()
|
||||
.eventTypeFqn(eventTypeFqn)
|
||||
.stateTypeFqn(stateTypeFqn)
|
||||
.event(canonicalizeLabel(trigger.getEvent(), eventTypeFqn))
|
||||
.sourceState(canonicalizeLabel(trigger.getSourceState(), stateTypeFqn))
|
||||
.polymorphicEvents(polymorphicEvents)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
|
||||
if (event == null || eventTypeFqn == null || event.isEmpty()) {
|
||||
return event;
|
||||
}
|
||||
if (event.startsWith("<SYMBOLIC: ")) {
|
||||
return event;
|
||||
}
|
||||
if (isWildcardVariable(event)) {
|
||||
return event;
|
||||
}
|
||||
if (Character.isLowerCase(event.charAt(0))) {
|
||||
return event;
|
||||
}
|
||||
if (!isEnumConstantCandidate(event)) {
|
||||
return event;
|
||||
}
|
||||
if (isStringOrPrimitiveType(eventTypeFqn)) {
|
||||
return event;
|
||||
}
|
||||
if (isMachineEnumReference(event, eventTypeFqn)) {
|
||||
return canonicalizeLabel(event, stripGenerics(eventTypeFqn));
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private static String preferFullTypeFqn(String primary, String fallback) {
|
||||
if (primary != null && primary.contains(".")) {
|
||||
return stripGenerics(primary);
|
||||
}
|
||||
if (fallback != null && fallback.contains(".")) {
|
||||
return stripGenerics(fallback);
|
||||
}
|
||||
return primary != null ? primary : fallback;
|
||||
}
|
||||
|
||||
private static String stripGenerics(String typeFqn) {
|
||||
int idx = typeFqn.indexOf('<');
|
||||
return idx >= 0 ? typeFqn.substring(0, idx) : typeFqn;
|
||||
}
|
||||
|
||||
public static boolean isStringOrPrimitiveType(String typeFqn) {
|
||||
if (typeFqn == null) {
|
||||
return false;
|
||||
}
|
||||
return typeFqn.equals("String") || typeFqn.equals("java.lang.String")
|
||||
|| typeFqn.equals("int") || typeFqn.equals("long") || typeFqn.equals("char")
|
||||
|| typeFqn.equals("java.lang.Object") || typeFqn.equals("Object");
|
||||
}
|
||||
|
||||
private static boolean isEnumConstantCandidate(String eventStr) {
|
||||
if (eventStr == null) {
|
||||
return false;
|
||||
}
|
||||
if (eventStr.startsWith("<SYMBOLIC: ")) {
|
||||
return false;
|
||||
}
|
||||
return !eventStr.contains(" ") && !eventStr.contains("(") && !eventStr.contains(")")
|
||||
&& !eventStr.contains("?");
|
||||
}
|
||||
|
||||
private static boolean isWildcardVariable(String eventStr) {
|
||||
if (eventStr == null) {
|
||||
return false;
|
||||
}
|
||||
if (eventStr.equals("event") || eventStr.equals("e")
|
||||
|| eventStr.equals("msg") || eventStr.equals("message")
|
||||
|| eventStr.equals("payload")) {
|
||||
return true;
|
||||
}
|
||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
||||
return true;
|
||||
}
|
||||
return eventStr.contains(" ? ") && eventStr.contains(" : ");
|
||||
}
|
||||
|
||||
private static List<State> canonicalizeStates(List<State> states, String stateTypeFqn) {
|
||||
if (states == null) {
|
||||
return null;
|
||||
}
|
||||
List<State> canonical = new ArrayList<>(states.size());
|
||||
for (State state : states) {
|
||||
canonical.add(canonicalizeState(state, stateTypeFqn));
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
|
||||
static Event canonicalizeEvent(Event event, String enumTypeFqn) {
|
||||
if (event == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||
return event;
|
||||
}
|
||||
String canonical = canonicalizeLabel(
|
||||
event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName(),
|
||||
enumTypeFqn);
|
||||
if (canonical.equals(event.fullIdentifier())) {
|
||||
return event;
|
||||
}
|
||||
return Event.of(event.rawName(), canonical);
|
||||
}
|
||||
|
||||
static State canonicalizeState(State state, String enumTypeFqn) {
|
||||
if (state == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||
return state;
|
||||
}
|
||||
String canonical = canonicalizeLabel(
|
||||
state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName(),
|
||||
enumTypeFqn);
|
||||
if (canonical.equals(state.fullIdentifier())) {
|
||||
return state;
|
||||
}
|
||||
return State.of(state.rawName(), canonical);
|
||||
}
|
||||
|
||||
public static boolean isMachineEnumReference(String value, String enumTypeFqn) {
|
||||
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (isStringOrPrimitiveType(enumTypeFqn)) {
|
||||
return false;
|
||||
}
|
||||
if (value.startsWith("<SYMBOLIC: ")) {
|
||||
return false;
|
||||
}
|
||||
if (value.length() >= 2 && value.startsWith("\"")) {
|
||||
return false;
|
||||
}
|
||||
if (!isEnumConstantCandidate(value)) {
|
||||
return false;
|
||||
}
|
||||
if (Character.isLowerCase(value.charAt(0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String typePart = enumTypeFromRef(value);
|
||||
if (typePart == null) {
|
||||
return true;
|
||||
}
|
||||
return enumTypesMatch(enumTypeFqn, typePart);
|
||||
}
|
||||
|
||||
public static String canonicalizeLabel(String value, String enumTypeFqn) {
|
||||
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
||||
return stripQuotes(value);
|
||||
}
|
||||
|
||||
String stripped = value;
|
||||
if (stripped == null || stripped.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (stripped.startsWith(enumTypeFqn + ".")) {
|
||||
return stripped;
|
||||
}
|
||||
|
||||
String constant = constantName(stripped);
|
||||
String typePart = enumTypeFromRef(stripped);
|
||||
|
||||
if (typePart != null && enumTypesMatch(enumTypeFqn, typePart)) {
|
||||
return enumTypeFqn + "." + constant;
|
||||
}
|
||||
|
||||
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
|
||||
return enumTypeFqn + "." + stripped;
|
||||
}
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
private static String stripQuotes(String value) {
|
||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String constantName(String ref) {
|
||||
int dot = ref.lastIndexOf('.');
|
||||
return dot >= 0 ? ref.substring(dot + 1) : ref;
|
||||
}
|
||||
|
||||
private static String enumTypeFromRef(String ref) {
|
||||
int dot = ref.lastIndexOf('.');
|
||||
if (dot <= 0) {
|
||||
return null;
|
||||
}
|
||||
return ref.substring(0, dot);
|
||||
}
|
||||
|
||||
static boolean enumTypesMatch(String type1, String type2) {
|
||||
if (type1 == null || type2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (type1.equals(type2)) {
|
||||
return true;
|
||||
}
|
||||
if (type1.endsWith("." + type2) || type2.endsWith("." + type1)) {
|
||||
return true;
|
||||
}
|
||||
String simple1 = simpleName(type1);
|
||||
String simple2 = simpleName(type2);
|
||||
if (!simple1.equals(simple2)) {
|
||||
return false;
|
||||
}
|
||||
return !type1.contains(".") || !type2.contains(".");
|
||||
}
|
||||
|
||||
private static String simpleName(String fqn) {
|
||||
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Resolves {@code StateMachineConfigurerAdapter<S, E>} type arguments for a config class.
|
||||
* Used by analysis (canonical enum ids) and routing (machine scoping).
|
||||
*/
|
||||
public final class StateMachineTypeResolver {
|
||||
|
||||
public record MachineTypes(String stateTypeFqn, String eventTypeFqn) {
|
||||
public static MachineTypes empty() {
|
||||
return new MachineTypes(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private StateMachineTypeResolver() {
|
||||
}
|
||||
|
||||
public static String[] resolve(String machineConfigFqn, CodebaseContext context) {
|
||||
MachineTypes types = resolveTypes(machineConfigFqn, context);
|
||||
return new String[]{types.stateTypeFqn(), types.eventTypeFqn()};
|
||||
}
|
||||
|
||||
public static MachineTypes resolveTypes(String machineConfigFqn, CodebaseContext context) {
|
||||
if (machineConfigFqn == null || context == null) {
|
||||
return MachineTypes.empty();
|
||||
}
|
||||
String configFqn = machineConfigFqn;
|
||||
if (configFqn.contains("#")) {
|
||||
configFqn = configFqn.substring(0, configFqn.indexOf('#'));
|
||||
}
|
||||
TypeDeclaration td = context.getTypeDeclaration(configFqn);
|
||||
if (td == null) {
|
||||
return MachineTypes.empty();
|
||||
}
|
||||
Set<String> visited = new HashSet<>();
|
||||
visited.add(configFqn);
|
||||
String[] args = findTypeArgumentsRecursively(
|
||||
td.getSuperclassType(),
|
||||
context,
|
||||
(CompilationUnit) td.getRoot(),
|
||||
visited,
|
||||
Map.of());
|
||||
return new MachineTypes(args[0], args[1]);
|
||||
}
|
||||
|
||||
public static boolean extendsEnumStateMachineConfigurer(String machineConfigFqn, CodebaseContext context) {
|
||||
if (machineConfigFqn == null || context == null) {
|
||||
return false;
|
||||
}
|
||||
String configFqn = machineConfigFqn;
|
||||
if (configFqn.contains("#")) {
|
||||
configFqn = configFqn.substring(0, configFqn.indexOf('#'));
|
||||
}
|
||||
TypeDeclaration td = context.getTypeDeclaration(configFqn);
|
||||
if (td == null) {
|
||||
return false;
|
||||
}
|
||||
Set<String> visited = new HashSet<>();
|
||||
visited.add(configFqn);
|
||||
return hierarchyHasEnumConfigurer(
|
||||
td.getSuperclassType(),
|
||||
context,
|
||||
(CompilationUnit) td.getRoot(),
|
||||
visited);
|
||||
}
|
||||
|
||||
private static boolean hierarchyHasEnumConfigurer(
|
||||
Type type,
|
||||
CodebaseContext context,
|
||||
CompilationUnit cu,
|
||||
Set<String> visited) {
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
if (type instanceof ParameterizedType pt) {
|
||||
String rawTypeName = pt.getType().toString();
|
||||
if (rawTypeName.equals("EnumStateMachineConfigurerAdapter")
|
||||
|| rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
||||
return true;
|
||||
}
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.add(fqn)) {
|
||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
||||
if (hierarchyHasEnumConfigurer(superclass, context, cu, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String simpleName = type.toString();
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.add(fqn)) {
|
||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
||||
if (hierarchyHasEnumConfigurer(superclass, context, cu, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String[] findTypeArgumentsRecursively(
|
||||
Type type,
|
||||
CodebaseContext context,
|
||||
CompilationUnit cu,
|
||||
Set<String> visited,
|
||||
Map<String, String> typeVarBindings) {
|
||||
if (type == null) {
|
||||
return new String[]{null, null};
|
||||
}
|
||||
|
||||
if (type instanceof ParameterizedType pt) {
|
||||
String rawTypeName = pt.getType().toString();
|
||||
List<?> typeArgs = pt.typeArguments();
|
||||
|
||||
if (isStateMachineConfigurerAdapter(rawTypeName)) {
|
||||
if (typeArgs.size() >= 2) {
|
||||
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu, context, typeVarBindings);
|
||||
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu, context, typeVarBindings);
|
||||
return new String[]{stateType, eventType};
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> nextBindings = extendBindings(rawTypeName, typeArgs, cu, context, typeVarBindings);
|
||||
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.add(fqn)) {
|
||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
||||
String[] res = findTypeArgumentsRecursively(superclass, context, cu, visited, nextBindings);
|
||||
if (res[0] != null || res[1] != null) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String simpleName = type.toString();
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.add(fqn)) {
|
||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
||||
String[] res = findTypeArgumentsRecursively(superclass, context, cu, visited, typeVarBindings);
|
||||
if (res[0] != null || res[1] != null) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{null, null};
|
||||
}
|
||||
|
||||
private static Map<String, String> extendBindings(
|
||||
String rawTypeName,
|
||||
List<?> typeArgs,
|
||||
CompilationUnit cu,
|
||||
CodebaseContext context,
|
||||
Map<String, String> parentBindings) {
|
||||
Map<String, String> bindings = new HashMap<>(parentBindings);
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
||||
if (!(td instanceof TypeDeclaration typeDecl) || typeArgs.isEmpty()) {
|
||||
return bindings;
|
||||
}
|
||||
List<TypeParameter> typeParameters = typeDecl.typeParameters();
|
||||
for (int i = 0; i < Math.min(typeParameters.size(), typeArgs.size()); i++) {
|
||||
String paramName = typeParameters.get(i).getName().getIdentifier();
|
||||
String resolvedArg = resolveTypeToFqn((Type) typeArgs.get(i), cu, context, parentBindings);
|
||||
if (resolvedArg != null) {
|
||||
bindings.put(paramName, resolvedArg);
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private static boolean isStateMachineConfigurerAdapter(String rawTypeName) {
|
||||
return rawTypeName.equals("StateMachineConfigurerAdapter")
|
||||
|| rawTypeName.equals("EnumStateMachineConfigurerAdapter")
|
||||
|| rawTypeName.endsWith(".StateMachineConfigurerAdapter")
|
||||
|| rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter");
|
||||
}
|
||||
|
||||
static String resolveTypeToFqn(Type type, CompilationUnit cu, CodebaseContext context) {
|
||||
return resolveTypeToFqn(type, cu, context, Map.of());
|
||||
}
|
||||
|
||||
static String resolveTypeToFqn(
|
||||
Type type,
|
||||
CompilationUnit cu,
|
||||
CodebaseContext context,
|
||||
Map<String, String> typeVarBindings) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
if (type instanceof WildcardType wildcardType) {
|
||||
Type bound = wildcardType.getBound();
|
||||
return bound != null ? resolveTypeToFqn(bound, cu, context, typeVarBindings) : null;
|
||||
}
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu, context, typeVarBindings);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
if (simpleName.contains("<")) {
|
||||
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
||||
}
|
||||
|
||||
if (typeVarBindings.containsKey(simpleName)) {
|
||||
return typeVarBindings.get(simpleName);
|
||||
}
|
||||
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
if (!packageName.isEmpty()) {
|
||||
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||
if (localTd != null) {
|
||||
return context.getFqn(localTd);
|
||||
}
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
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.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Re-applies machine-scoped enum canonicalization and validates the analysis model.
|
||||
* Must run after property resolution so placeholders cannot bypass validation.
|
||||
*/
|
||||
public final class AnalysisResultFinalizer {
|
||||
|
||||
private AnalysisResultFinalizer() {
|
||||
}
|
||||
|
||||
public static void finalizeResult(AnalysisResult result, CodebaseContext context) {
|
||||
if (result == null || context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
|
||||
MachineEnumCanonicalizer.canonicalizeTransitions(result.getTransitions(), machineTypes);
|
||||
|
||||
if (result.getStates() != null) {
|
||||
result.setStates(MachineEnumCanonicalizer.canonicalizeStates(result.getStates(), machineTypes.stateTypeFqn()));
|
||||
}
|
||||
result.setStartStates(MachineEnumCanonicalizer.canonicalizeStateLabels(
|
||||
result.getStartStates(), machineTypes.stateTypeFqn()));
|
||||
result.setEndStates(MachineEnumCanonicalizer.canonicalizeStateLabels(
|
||||
result.getEndStates(), machineTypes.stateTypeFqn()));
|
||||
|
||||
if (result.getMetadata() != null) {
|
||||
CodebaseMetadata metadata = result.getMetadata();
|
||||
List<TriggerPoint> triggers = canonicalizeTriggers(metadata.getTriggers(), machineTypes);
|
||||
List<CallChain> callChains = canonicalizeCallChains(metadata.getCallChains(), machineTypes);
|
||||
result.setMetadata(CodebaseMetadata.builder()
|
||||
.triggers(triggers)
|
||||
.entryPoints(metadata.getEntryPoints())
|
||||
.callChains(callChains)
|
||||
.properties(metadata.getProperties())
|
||||
.build());
|
||||
}
|
||||
|
||||
AnalysisCanonicalFormValidator.enforce(result, context);
|
||||
}
|
||||
|
||||
private static List<TriggerPoint> canonicalizeTriggers(
|
||||
List<TriggerPoint> triggers,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
if (triggers == null) {
|
||||
return null;
|
||||
}
|
||||
return triggers.stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<CallChain> canonicalizeCallChains(
|
||||
List<CallChain> callChains,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
if (callChains == null) {
|
||||
return null;
|
||||
}
|
||||
return callChains.stream()
|
||||
.map(chain -> {
|
||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
||||
? null
|
||||
: MachineEnumCanonicalizer.canonicalizeTriggerPoint(chain.getTriggerPoint(), machineTypes);
|
||||
List<MatchedTransition> matched = canonicalizeMatchedTransitions(
|
||||
chain.getMatchedTransitions(), machineTypes);
|
||||
return chain.toBuilder()
|
||||
.triggerPoint(trigger)
|
||||
.matchedTransitions(matched)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<MatchedTransition> canonicalizeMatchedTransitions(
|
||||
List<MatchedTransition> matchedTransitions,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
if (matchedTransitions == null) {
|
||||
return null;
|
||||
}
|
||||
return matchedTransitions.stream()
|
||||
.map(matched -> MatchedTransition.builder()
|
||||
.event(MachineEnumCanonicalizer.canonicalizeLabel(
|
||||
matched.getEvent(), machineTypes.eventTypeFqn()))
|
||||
.sourceState(MachineEnumCanonicalizer.canonicalizeLabel(
|
||||
matched.getSourceState(), machineTypes.stateTypeFqn()))
|
||||
.targetState(MachineEnumCanonicalizer.canonicalizeLabel(
|
||||
matched.getTargetState(), machineTypes.stateTypeFqn()))
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.validation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Thrown when {@link AnalysisCanonicalFormValidator} finds enum identifiers that are not
|
||||
* package-canonical for the machine's resolved {@code <State, Event>} types.
|
||||
*/
|
||||
public class AnalysisCanonicalFormException extends IllegalStateException {
|
||||
|
||||
private final List<AnalysisCanonicalFormValidator.Violation> violations;
|
||||
|
||||
public AnalysisCanonicalFormException(List<AnalysisCanonicalFormValidator.Violation> violations) {
|
||||
super(formatMessage(violations));
|
||||
this.violations = List.copyOf(violations);
|
||||
}
|
||||
|
||||
public List<AnalysisCanonicalFormValidator.Violation> getViolations() {
|
||||
return violations;
|
||||
}
|
||||
|
||||
private static String formatMessage(List<AnalysisCanonicalFormValidator.Violation> violations) {
|
||||
StringBuilder sb = new StringBuilder("Analysis model contains non-canonical enum identifiers:");
|
||||
for (AnalysisCanonicalFormValidator.Violation violation : violations) {
|
||||
sb.append("\n - ").append(violation.path())
|
||||
.append(": '").append(violation.actual())
|
||||
.append("' (expected '").append(violation.expected()).append("')");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.validation;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
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.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Validates that enum-backed identifiers across the analysis model share one canonical shape:
|
||||
* {@code {package.EnumType.CONSTANT}} derived from the machine config's {@code <State, Event>} types.
|
||||
*
|
||||
* <p>String/primitive machines ({@code StateMachineConfigurerAdapter<String, String>}) are skipped.
|
||||
*/
|
||||
public final class AnalysisCanonicalFormValidator {
|
||||
|
||||
public record Violation(String path, String actual, String expected) {
|
||||
}
|
||||
|
||||
private AnalysisCanonicalFormValidator() {
|
||||
}
|
||||
|
||||
public static List<Violation> validate(AnalysisResult result, CodebaseContext context) {
|
||||
List<Violation> violations = new ArrayList<>();
|
||||
if (result == null || context == null) {
|
||||
return violations;
|
||||
}
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
if (!hasPackageQualifiedEnumType(machineTypes.stateTypeFqn())
|
||||
&& !hasPackageQualifiedEnumType(machineTypes.eventTypeFqn())) {
|
||||
if (!StateMachineTypeResolver.extendsEnumStateMachineConfigurer(result.getName(), context)) {
|
||||
return violations;
|
||||
}
|
||||
violations.add(new Violation(
|
||||
"machineTypes",
|
||||
describeTypes(machineTypes),
|
||||
"package-qualified state/event enum types"));
|
||||
return violations;
|
||||
}
|
||||
|
||||
validateTransitions(result.getTransitions(), machineTypes, violations);
|
||||
validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
|
||||
validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations);
|
||||
validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations);
|
||||
|
||||
if (result.getMetadata() != null) {
|
||||
validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations);
|
||||
validateCallChains(result.getMetadata().getCallChains(), machineTypes, violations);
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
public static void enforce(AnalysisResult result, CodebaseContext context) {
|
||||
List<Violation> violations = validate(result, context);
|
||||
if (!violations.isEmpty()) {
|
||||
throw new AnalysisCanonicalFormException(violations);
|
||||
}
|
||||
}
|
||||
|
||||
private static String describeTypes(StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
return "stateTypeFqn=" + machineTypes.stateTypeFqn() + ", eventTypeFqn=" + machineTypes.eventTypeFqn();
|
||||
}
|
||||
|
||||
private static boolean hasPackageQualifiedEnumType(String typeFqn) {
|
||||
return typeFqn != null && typeFqn.contains(".") && !MachineEnumCanonicalizer.isStringOrPrimitiveType(typeFqn);
|
||||
}
|
||||
|
||||
private static void validateTransitions(
|
||||
List<Transition> transitions,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
List<Violation> violations) {
|
||||
if (transitions == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < transitions.size(); i++) {
|
||||
Transition transition = transitions.get(i);
|
||||
String prefix = "transitions[" + i + "]";
|
||||
if (transition.getEvent() != null) {
|
||||
requireCanonical(
|
||||
prefix + ".event.fullIdentifier",
|
||||
transition.getEvent().fullIdentifier(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
violations);
|
||||
}
|
||||
validateStateList(transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
|
||||
validateStateList(transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateStateList(
|
||||
List<State> states,
|
||||
String stateTypeFqn,
|
||||
String pathPrefix,
|
||||
List<Violation> violations) {
|
||||
if (states == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < states.size(); i++) {
|
||||
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", states.get(i).fullIdentifier(), stateTypeFqn, violations);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateStateCollection(
|
||||
Set<State> states,
|
||||
String stateTypeFqn,
|
||||
String pathPrefix,
|
||||
List<Violation> violations) {
|
||||
if (states == null) {
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (State state : states) {
|
||||
requireCanonical(pathPrefix + "[" + i++ + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateStateLabels(
|
||||
Set<String> labels,
|
||||
String stateTypeFqn,
|
||||
String pathPrefix,
|
||||
List<Violation> violations) {
|
||||
if (labels == null) {
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (String label : labels) {
|
||||
requireCanonical(pathPrefix + "[" + i++ + "]", label, stateTypeFqn, violations);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTriggers(
|
||||
List<TriggerPoint> triggers,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
List<Violation> violations) {
|
||||
if (triggers == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < triggers.size(); i++) {
|
||||
TriggerPoint trigger = triggers.get(i);
|
||||
String prefix = "metadata.triggers[" + i + "]";
|
||||
String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
|
||||
String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
|
||||
|
||||
requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
|
||||
requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
|
||||
|
||||
if (trigger.getPolymorphicEvents() != null) {
|
||||
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
|
||||
requireCanonical(
|
||||
prefix + ".polymorphicEvents[" + j + "]",
|
||||
trigger.getPolymorphicEvents().get(j),
|
||||
eventTypeFqn,
|
||||
violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateCallChains(
|
||||
List<CallChain> callChains,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
List<Violation> violations) {
|
||||
if (callChains == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < callChains.size(); i++) {
|
||||
CallChain chain = callChains.get(i);
|
||||
String prefix = "metadata.callChains[" + i + "]";
|
||||
TriggerPoint trigger = chain.getTriggerPoint();
|
||||
if (trigger != null) {
|
||||
String triggerPrefix = prefix + ".triggerPoint";
|
||||
String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
|
||||
String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
|
||||
requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
|
||||
requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
|
||||
if (trigger.getPolymorphicEvents() != null) {
|
||||
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
|
||||
requireCanonical(
|
||||
triggerPrefix + ".polymorphicEvents[" + j + "]",
|
||||
trigger.getPolymorphicEvents().get(j),
|
||||
eventTypeFqn,
|
||||
violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null) {
|
||||
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
|
||||
MatchedTransition matched = chain.getMatchedTransitions().get(j);
|
||||
String matchedPrefix = prefix + ".matchedTransitions[" + j + "]";
|
||||
requireCanonical(matchedPrefix + ".event", matched.getEvent(), machineTypes.eventTypeFqn(), violations);
|
||||
requireCanonical(matchedPrefix + ".sourceState", matched.getSourceState(), machineTypes.stateTypeFqn(), violations);
|
||||
requireCanonical(matchedPrefix + ".targetState", matched.getTargetState(), machineTypes.stateTypeFqn(), violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String preferTypeFqn(String primary, String fallback) {
|
||||
if (hasPackageQualifiedEnumType(primary)) {
|
||||
return primary;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static void requireCanonical(
|
||||
String path,
|
||||
String value,
|
||||
String enumTypeFqn,
|
||||
List<Violation> violations) {
|
||||
if (!MachineEnumCanonicalizer.isMachineEnumReference(value, enumTypeFqn)) {
|
||||
return;
|
||||
}
|
||||
String expected = MachineEnumCanonicalizer.canonicalizeLabel(value, enumTypeFqn);
|
||||
if (!expected.equals(value)) {
|
||||
violations.add(new Violation(path, value, expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package click.kamil.springstatemachineexporter.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.AnalysisResultFinalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
||||
@@ -47,6 +50,7 @@ public class ExportService {
|
||||
new PropertyEnricher(),
|
||||
new CallChainEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
||||
)));
|
||||
}
|
||||
@@ -233,16 +237,23 @@ public class ExportService {
|
||||
StateMachineAggregator aggregator = new StateMachineAggregator(context);
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes = StateMachineTypeResolver.resolveTypes(className, context);
|
||||
MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes);
|
||||
|
||||
aggregator.aggregateStates(td);
|
||||
Set<String> initialStatesAst = aggregator.getInitialStates();
|
||||
Set<String> endStatesAst = aggregator.getEndStates();
|
||||
Set<String> initialStatesAst = MachineEnumCanonicalizer.canonicalizeStateLabels(
|
||||
aggregator.getInitialStates(), machineTypes.stateTypeFqn());
|
||||
Set<String> endStatesAst = MachineEnumCanonicalizer.canonicalizeStateLabels(
|
||||
aggregator.getEndStates(), machineTypes.stateTypeFqn());
|
||||
|
||||
log.debug("Start States Ast: {}", initialStatesAst);
|
||||
log.debug("End States Ast: {}", endStatesAst);
|
||||
|
||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
|
||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
|
||||
Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst);
|
||||
Set<click.kamil.springstatemachineexporter.model.State> allStates = MachineEnumCanonicalizer.canonicalizeStates(
|
||||
TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst),
|
||||
machineTypes.stateTypeFqn());
|
||||
|
||||
if (allStates.isEmpty() && transitions.isEmpty()) {
|
||||
log.info("Skipping empty state machine config: {}", className);
|
||||
@@ -262,6 +273,7 @@ public class ExportService {
|
||||
enrichmentService.enrich(result, context, intelligence);
|
||||
|
||||
resolveProperties(result, activeProfiles);
|
||||
AnalysisResultFinalizer.finalizeResult(result, context);
|
||||
|
||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||
}
|
||||
@@ -276,9 +288,14 @@ public class ExportService {
|
||||
|
||||
List<Transition> transitions = AstTransitionParser.parseTransitions(m, context);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes = StateMachineTypeResolver.resolveTypes(parentFqn, context);
|
||||
MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes);
|
||||
|
||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, null);
|
||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, null);
|
||||
Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, null, null);
|
||||
Set<click.kamil.springstatemachineexporter.model.State> allStates = MachineEnumCanonicalizer.canonicalizeStates(
|
||||
TransitionStateUtils.findAllStates(transitions, null, null),
|
||||
machineTypes.stateTypeFqn());
|
||||
|
||||
if (allStates.isEmpty() && transitions.isEmpty()) {
|
||||
log.info("Skipping empty state machine bean: {}", uniqueName);
|
||||
@@ -298,6 +315,7 @@ public class ExportService {
|
||||
enrichmentService.enrich(result, context, intelligence);
|
||||
|
||||
resolveProperties(result, activeProfiles);
|
||||
AnalysisResultFinalizer.finalizeResult(result, context);
|
||||
|
||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user