Compare commits
11 Commits
a383de5a5f
...
still_miss
| Author | SHA1 | Date | |
|---|---|---|---|
| d59f4ac166 | |||
| 19547d3c5f | |||
| ab37eb5d40 | |||
| bf9208d529 | |||
| 7077214c81 | |||
| 344e295106 | |||
| d6b1571f18 | |||
| e617fb1754 | |||
| 06dc0ba641 | |||
| ba412b905e | |||
| 198c5d830e |
3
TODO.md
3
TODO.md
@@ -1,10 +1,9 @@
|
||||
1.extract from all possible formats (ask chatgpt)
|
||||
|
||||
[DONE] json
|
||||
[TODO] xml, maybe yaml
|
||||
|
||||
2. [PLAN] Support external dependencies (JARs/compiled classes):
|
||||
- Enable binding resolution in ASTParser (`setResolveBindings(true)`).
|
||||
- Configure ASTParser with project classpath (including JARs).
|
||||
- Refactor `CodebaseContext` to handle resolved bindings, not just local AST nodes.
|
||||
- Implement tests using external libraries as base classes.
|
||||
- Implement tests using external libraries as base classes.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -36,6 +36,7 @@ dependencies {
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.46'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.46'
|
||||
|
||||
@@ -14,6 +14,14 @@ import java.util.List;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// Enable diagnostic mode early before SLF4J initializes
|
||||
for (String arg : args) {
|
||||
if ("--debug".equals(arg)) {
|
||||
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual DI / Wiring
|
||||
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||
var exportService = new ExportService(exporters);
|
||||
|
||||
@@ -41,20 +41,62 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
if (t.getEvent() != null) {
|
||||
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
|
||||
String smEvent = simplify(smEventRaw);
|
||||
if (smEvent.equals(triggerEvent)) {
|
||||
// Event matches. Check source state if provided
|
||||
|
||||
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
|
||||
triggerEvent.equals("msg") || triggerEvent.equals("message") ||
|
||||
triggerEvent.equals("payload");
|
||||
|
||||
if (isWildcard) {
|
||||
String targetVar = chain.getContextMachineId();
|
||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
// We no longer hard-block wildcards without a specific routing context.
|
||||
// If a project doesn't use standard SM persisters (e.g. restores state manually),
|
||||
// contextMachineId will be null. We should still link the wildcard to provide SOME visibility,
|
||||
// rather than completely hiding the endpoint.
|
||||
}
|
||||
|
||||
List<String> polyEvents = tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||
boolean hasPolyMatch = false;
|
||||
for (String pe : polyEvents) {
|
||||
String simplePe = pe;
|
||||
if (pe.contains(".")) {
|
||||
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
||||
}
|
||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
|
||||
hasPolyMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) {
|
||||
// Event matches or is a wildcard
|
||||
for (State smSourceState : t.getSourceStates()) {
|
||||
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
||||
String smSource = simplify(smSourceRaw);
|
||||
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||
for (State smTargetState : t.getTargetStates()) {
|
||||
String sourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
||||
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||
matched.add(MatchedTransition.builder()
|
||||
.sourceState(sourceRaw)
|
||||
.targetState(targetRaw)
|
||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||
MatchedTransition mt = MatchedTransition.builder()
|
||||
.sourceState(smSourceRaw)
|
||||
.targetState(smSourceRaw)
|
||||
.event(smEventRaw)
|
||||
.build());
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
} else {
|
||||
for (State smTargetState : t.getTargetStates()) {
|
||||
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||
MatchedTransition mt = MatchedTransition.builder()
|
||||
.sourceState(smSourceRaw)
|
||||
.targetState(targetRaw)
|
||||
.event(smEventRaw)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,16 +104,12 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new CallChain with the matched transitions
|
||||
CallChain updatedChain = CallChain.builder()
|
||||
.entryPoint(chain.getEntryPoint())
|
||||
.methodChain(chain.getMethodChain())
|
||||
.triggerPoint(chain.getTriggerPoint())
|
||||
.contextMachineId(chain.getContextMachineId())
|
||||
.matchedTransitions(matched)
|
||||
.build();
|
||||
|
||||
updatedChains.add(updatedChain);
|
||||
if (!matched.isEmpty()) {
|
||||
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
|
||||
updatedChains.add(newChain);
|
||||
} else {
|
||||
updatedChains.add(chain);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the metadata with the new call chains
|
||||
@@ -85,12 +123,38 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
result.setMetadata(updatedMetadata);
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
if (name == null) return "";
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot >= 0) {
|
||||
return name.substring(dot + 1);
|
||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||
// If the chain's target expression or qualifier indicates a specific machine name, verify it
|
||||
String targetVar = chain.getContextMachineId();
|
||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
return name;
|
||||
|
||||
if (targetVar != null && !targetVar.isEmpty()) {
|
||||
targetVar = targetVar.toLowerCase();
|
||||
// E.g., if target is "myStateMachine", ensure current machine name contains "my"
|
||||
String simplifiedMachineName = currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase();
|
||||
// If the variable name ends with StateMachine, we extract the prefix
|
||||
if (targetVar.endsWith("statemachine")) {
|
||||
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
|
||||
if (!prefix.isEmpty() && !simplifiedMachineName.contains(prefix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
if (name == null) return null;
|
||||
// Strip common suffixes
|
||||
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||
if (simplified.isEmpty()) {
|
||||
simplified = name;
|
||||
}
|
||||
// Simplify full identifiers to just the last part (enum name)
|
||||
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
|
||||
return simplified;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@Builder(toBuilder = true)
|
||||
@Jacksonized
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CallChain {
|
||||
|
||||
@@ -13,5 +13,7 @@ import java.util.List;
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class LibraryHint {
|
||||
private final String methodFqn; // e.g., "com.thirdparty.Workflow.send"
|
||||
private final String event; // The event it triggers
|
||||
private final String event; // The event it triggers (static)
|
||||
private final Integer eventArgumentIndex; // e.g., 0 to extract from the 0th argument
|
||||
private final String eventArgumentMethod; // e.g., "getType" to extract from argument method call
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@ public class TriggerPoint {
|
||||
private final String stateMachineId; // Optional: to link to a specific SM instance
|
||||
private final String sourceState; // Optional: if we can determine the expected current state
|
||||
private final int lineNumber;
|
||||
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
||||
}
|
||||
|
||||
@@ -48,12 +48,37 @@ public class ConstantResolver {
|
||||
return resolveInfix(infix, context, visited);
|
||||
}
|
||||
if (expr instanceof QualifiedName qn) {
|
||||
return resolveManual(qn, context, visited);
|
||||
String val = resolveManual(qn, context, visited);
|
||||
return val != null ? val : qn.toString();
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return resolveManual(sn, context, visited);
|
||||
}
|
||||
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
IMethodBinding mb = mi.resolveMethodBinding();
|
||||
if (mb != null) {
|
||||
ITypeBinding returnType = mb.getReturnType();
|
||||
if (returnType != null) {
|
||||
java.util.List<String> values = context.getEnumValues(returnType.getQualifiedName());
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return "ENUM_SET:" + String.join(",", values);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TypeDeclaration td = findEnclosingType(mi);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||
if (md != null && md.getReturnType2() != null) {
|
||||
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return "ENUM_SET:" + String.join(",", values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,25 +105,32 @@ public class PropertyResolver {
|
||||
}
|
||||
|
||||
private Map<String, String> loadYaml(Path path) {
|
||||
// Placeholder for future YAML support
|
||||
log.warn("YAML parsing not fully implemented yet for {}", path);
|
||||
return new HashMap<>();
|
||||
Map<String, String> props = new HashMap<>();
|
||||
try {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(new com.fasterxml.jackson.dataformat.yaml.YAMLFactory());
|
||||
Map<String, Object> map = mapper.readValue(path.toFile(), new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
|
||||
flattenYaml("", map, props);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to load YAML from {}", path);
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void flattenYaml(String prefix, Map<String, Object> map, Map<String, String> result) {
|
||||
if (map == null) return;
|
||||
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map) {
|
||||
flattenYaml(key, (Map<String, Object>) value, result);
|
||||
} else if (value != null) {
|
||||
result.put(key, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveValue(String placeholder, Map<String, String> properties) {
|
||||
if (placeholder == null || !placeholder.contains("${")) return placeholder;
|
||||
|
||||
// Very basic placeholder extraction: ${key} or ${key:default}
|
||||
String content = placeholder.substring(placeholder.indexOf("${") + 2, placeholder.lastIndexOf("}"));
|
||||
String key = content;
|
||||
String defaultValue = null;
|
||||
|
||||
if (content.contains(":")) {
|
||||
int colonIndex = content.indexOf(":");
|
||||
key = content.substring(0, colonIndex);
|
||||
defaultValue = content.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
return properties.getOrDefault(key, defaultValue);
|
||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(placeholder, properties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,12 @@ public class CallGraphBuilder {
|
||||
|
||||
for (EntryPoint ep : entryPoints) {
|
||||
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
||||
boolean foundAny = false;
|
||||
for (TriggerPoint tp : triggers) {
|
||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
||||
if (path != null) {
|
||||
foundAny = true;
|
||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||
String contextMachineId = extractContextMachineId(path, callGraph);
|
||||
chains.add(CallChain.builder()
|
||||
@@ -48,6 +50,9 @@ public class CallGraphBuilder {
|
||||
.build());
|
||||
}
|
||||
}
|
||||
if (!foundAny && log.isDebugEnabled()) {
|
||||
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
|
||||
}
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
@@ -60,6 +65,17 @@ public class CallGraphBuilder {
|
||||
|
||||
String currentParamName = event;
|
||||
String resolvedValue = event;
|
||||
String methodSuffix = "";
|
||||
|
||||
// Extract method calls like .getType() so we can trace the base parameter
|
||||
int dotIndex = currentParamName.indexOf('.');
|
||||
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
|
||||
char nextChar = currentParamName.charAt(dotIndex + 1);
|
||||
if (Character.isLowerCase(nextChar)) {
|
||||
methodSuffix = currentParamName.substring(dotIndex);
|
||||
currentParamName = currentParamName.substring(0, dotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk backwards up the call chain
|
||||
for (int i = path.size() - 1; i > 0; i--) {
|
||||
@@ -68,6 +84,21 @@ public class CallGraphBuilder {
|
||||
|
||||
// Find parameter index in target method
|
||||
int paramIndex = getParameterIndex(target, currentParamName);
|
||||
if (paramIndex < 0) {
|
||||
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
|
||||
String tracedVar = traceLocalVariable(target, currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
// Extract method calls like .getType() from the traced variable
|
||||
int dotIdx = tracedVar.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||
tracedVar = tracedVar.substring(0, dotIdx);
|
||||
}
|
||||
currentParamName = tracedVar;
|
||||
resolvedValue = tracedVar + methodSuffix;
|
||||
paramIndex = getParameterIndex(target, currentParamName);
|
||||
}
|
||||
}
|
||||
if (paramIndex < 0) {
|
||||
break; // Parameter name changed or not found, stop tracing
|
||||
}
|
||||
@@ -81,8 +112,14 @@ public class CallGraphBuilder {
|
||||
if (paramIndex < edge.getArguments().size()) {
|
||||
String arg = edge.getArguments().get(paramIndex);
|
||||
if (arg != null) {
|
||||
// If the argument passed has a method call, extract it
|
||||
int dotIdx = arg.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
|
||||
methodSuffix = arg.substring(dotIdx) + methodSuffix;
|
||||
arg = arg.substring(0, dotIdx);
|
||||
}
|
||||
currentParamName = arg;
|
||||
resolvedValue = arg;
|
||||
resolvedValue = arg + methodSuffix;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -93,13 +130,84 @@ public class CallGraphBuilder {
|
||||
if (!found) break; // Could not map argument
|
||||
}
|
||||
|
||||
if (!resolvedValue.equals(event)) {
|
||||
// Final check on the entry method
|
||||
String entryMethod = path.get(0);
|
||||
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
|
||||
if (entryParamIndex < 0) {
|
||||
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
int dotIdx = tracedVar.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||
tracedVar = tracedVar.substring(0, dotIdx);
|
||||
}
|
||||
currentParamName = tracedVar;
|
||||
resolvedValue = tracedVar + methodSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> polymorphicEvents = new ArrayList<>();
|
||||
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
|
||||
int lastDot = resolvedValue.lastIndexOf('.');
|
||||
int firstDot = resolvedValue.indexOf('.');
|
||||
int openParen = resolvedValue.indexOf('(', lastDot);
|
||||
|
||||
if (lastDot > 0 && openParen > lastDot) {
|
||||
String varName = resolvedValue.substring(0, firstDot);
|
||||
if (varName.contains("(")) {
|
||||
varName = null;
|
||||
}
|
||||
String methodName = resolvedValue.substring(lastDot + 1, openParen);
|
||||
|
||||
if (varName != null) {
|
||||
// Resolve in the first method in the path where the variable might be declared
|
||||
for (String methodFqn : path) {
|
||||
String declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||
if (declaredType != null) {
|
||||
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType);
|
||||
List<String> typesToInspect = new ArrayList<>();
|
||||
typesToInspect.add(declaredType);
|
||||
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
|
||||
|
||||
for (String type : typesToInspect) {
|
||||
Set<String> visited = new HashSet<>();
|
||||
// We must find the compilation unit to pass for simple names!
|
||||
// Let's pass the first available CU for this class
|
||||
TypeDeclaration baseTd = context.getTypeDeclaration(type);
|
||||
CompilationUnit cuToUse = null;
|
||||
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
|
||||
cuToUse = (CompilationUnit) baseTd.getRoot();
|
||||
} else {
|
||||
String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
|
||||
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
|
||||
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||
}
|
||||
}
|
||||
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
||||
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
|
||||
for (String constant : constants) {
|
||||
if (!polymorphicEvents.contains(constant)) {
|
||||
polymorphicEvents.add(constant);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(polymorphicEvents)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -125,6 +233,172 @@ public class CallGraphBuilder {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private String getVariableDeclaredType(String methodFqn, String varName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
for (Object pObj : md.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return svd.getType().toString();
|
||||
}
|
||||
}
|
||||
final String[] foundType = new String[1];
|
||||
if (md.getBody() != null) {
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
foundType[0] = node.getType().toString();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
return foundType[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||
if (depth > 20) return Collections.emptyList();
|
||||
String fqn = className + "." + methodName;
|
||||
if (!visited.add(fqn)) return Collections.emptyList();
|
||||
|
||||
List<String> constants = new ArrayList<>();
|
||||
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
||||
if (td == null) {
|
||||
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
Expression retExpr = node.getExpression();
|
||||
if (retExpr != null) {
|
||||
boolean handled = false;
|
||||
if (retExpr instanceof MethodInvocation mi) {
|
||||
// Follow delegation first
|
||||
String called = resolveCalledMethod(mi);
|
||||
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
|
||||
if (called != null && called.contains(".")) {
|
||||
if (visited.contains(called)) {
|
||||
handled = true;
|
||||
} else {
|
||||
String cName = called.substring(0, called.lastIndexOf('.'));
|
||||
String mName = called.substring(called.lastIndexOf('.') + 1);
|
||||
|
||||
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||
|
||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||
constants.addAll(delegationResult);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!handled) {
|
||||
String val = constantResolver.resolve(retExpr, context);
|
||||
if (val != null) {
|
||||
if (val.startsWith("ENUM_SET:")) {
|
||||
for (String eVal : val.substring(9).split(",")) {
|
||||
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||
}
|
||||
} else {
|
||||
constants.add(val);
|
||||
}
|
||||
} else if (retExpr instanceof QualifiedName qn) {
|
||||
constants.add(qn.toString());
|
||||
} else if (retExpr instanceof SimpleName sn) {
|
||||
constants.add(sn.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
visited.remove(fqn);
|
||||
return constants;
|
||||
}
|
||||
|
||||
private String traceLocalVariable(String methodFqn, String varName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
final Expression[] initializer = new Expression[1];
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||
initializer[0] = node.getInitializer();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||
initializer[0] = node.getRightHandSide();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (initializer[0] != null) {
|
||||
Expression expr = traceVariable(initializer[0]);
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
|
||||
Expression innerMost = unwrapMethodInvocation(mi, 0);
|
||||
if (innerMost instanceof MethodInvocation innerMi) {
|
||||
if (innerMi.getExpression() instanceof SimpleName sn) {
|
||||
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
|
||||
}
|
||||
return innerMi.getName().getIdentifier() + "()";
|
||||
}
|
||||
if (innerMost instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return innerMost.toString();
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return expr.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||
if (depth > 5) return mi;
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
for (String node : path) {
|
||||
List<CallEdge> edges = callGraph.get(node);
|
||||
@@ -143,26 +417,62 @@ public class CallGraphBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
|
||||
private Map<String, List<CallEdge>> buildCallGraph() {
|
||||
graph = new HashMap<>();
|
||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (currentMethodFqn != null) {
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
MethodDeclaration md = findEnclosingMethod(node);
|
||||
if (md != null) {
|
||||
TypeDeclaration td = findEnclosingType(md);
|
||||
if (td != null) {
|
||||
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
String typeName = emr.getExpression().toString();
|
||||
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||
TypeDeclaration td2 = findEnclosingType(node);
|
||||
if (td2 != null) {
|
||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||
if (refMethod != null) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String fallbackTypeFqn = null;
|
||||
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
|
||||
if (binding != null) {
|
||||
fallbackTypeFqn = binding.getQualifiedName();
|
||||
} else if (emr.getExpression() instanceof SimpleName sn) {
|
||||
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||
}
|
||||
if (fallbackTypeFqn != null) {
|
||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
@@ -170,22 +480,27 @@ public class CallGraphBuilder {
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
if (currentMethodFqn != null) {
|
||||
String methodName = node.getName().getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
String calledMethod = null;
|
||||
if (superTd != null) {
|
||||
calledMethod = resolveMethodInType(superTd, methodName);
|
||||
MethodDeclaration md = findEnclosingMethod(node);
|
||||
if (md != null) {
|
||||
TypeDeclaration tdOuter = findEnclosingType(md);
|
||||
if (tdOuter != null) {
|
||||
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
String calledMethod = null;
|
||||
if (superTd != null) {
|
||||
calledMethod = resolveMethodInType(superTd, methodName);
|
||||
}
|
||||
if (calledMethod == null) {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
if (calledMethod == null) {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,12 +530,33 @@ public class CallGraphBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expr instanceof ExpressionMethodReference emr) {
|
||||
TypeDeclaration td = findEnclosingType(expr);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
for (Object stmtObj : md.getBody().statements()) {
|
||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||
expr = rs.getExpression();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||
expr = traceVariable(expr);
|
||||
if (expr instanceof ClassInstanceCreation cic) {
|
||||
if (!cic.arguments().isEmpty()) {
|
||||
expr = (Expression) cic.arguments().get(0);
|
||||
Expression tracedExpr = traceVariable(expr);
|
||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||
}
|
||||
|
||||
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
|
||||
Expression firstArg = (Expression) cic.arguments().get(0);
|
||||
String resolved = constantResolver.resolve(firstArg, context);
|
||||
if (resolved != null) {
|
||||
expr = firstArg; // Only unwrap if it's actually a constant
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +606,18 @@ public class CallGraphBuilder {
|
||||
return binding.getQualifiedName() + "." + methodName;
|
||||
}
|
||||
|
||||
if (receiver instanceof ThisExpression) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return context.getFqn(td) + "." + methodName;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||
if (fallbackTypeFqn != null) {
|
||||
return fallbackTypeFqn + "." + methodName;
|
||||
}
|
||||
String receiverName = sn.getIdentifier();
|
||||
return receiverName + "." + methodName;
|
||||
}
|
||||
@@ -278,6 +625,85 @@ public class CallGraphBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||
String varName = receiverNameNode.getIdentifier();
|
||||
|
||||
// 1. Check local variables in enclosing method
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||
if (enclosingMethod != null) {
|
||||
// Check parameters
|
||||
for (Object paramObj : enclosingMethod.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
// Check method body (local variables)
|
||||
if (enclosingMethod.getBody() != null) {
|
||||
Type[] foundType = new Type[1];
|
||||
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
foundType[0] = node.getType();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (foundType[0] != null) {
|
||||
return resolveTypeToFqn(foundType[0], receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check fields in enclosing class
|
||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||
if (enclosingType != null) {
|
||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||
if (type == null) return null;
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
@@ -307,6 +733,11 @@ public class CallGraphBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Path search dead-end at {} when looking for {}", start, target);
|
||||
}
|
||||
|
||||
visited.remove(start);
|
||||
return null;
|
||||
}
|
||||
@@ -354,12 +785,4 @@ public class CallGraphBuilder {
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,12 @@ public class GenericEventDetector {
|
||||
}
|
||||
|
||||
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||
TriggerPoint trigger = buildTriggerPoint(node, cu, null);
|
||||
if (trigger != null) {
|
||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||
triggers.add(trigger);
|
||||
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||
if (builtTriggers != null) {
|
||||
for (TriggerPoint trigger : builtTriggers) {
|
||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||
triggers.add(trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +64,23 @@ public class GenericEventDetector {
|
||||
|
||||
for (LibraryHint hint : hints) {
|
||||
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
||||
TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent());
|
||||
if (trigger != null) {
|
||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||
triggers.add(trigger);
|
||||
String eventToUse = hint.getEvent();
|
||||
if (eventToUse == null && hint.getEventArgumentIndex() != null) {
|
||||
if (node.arguments().size() > hint.getEventArgumentIndex()) {
|
||||
Expression argExpr = (Expression) node.arguments().get(hint.getEventArgumentIndex());
|
||||
eventToUse = argExpr.toString();
|
||||
if (hint.getEventArgumentMethod() != null && !hint.getEventArgumentMethod().isEmpty()) {
|
||||
eventToUse = eventToUse + "." + hint.getEventArgumentMethod() + "()";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
|
||||
if (builtTriggers != null) {
|
||||
for (TriggerPoint trigger : builtTriggers) {
|
||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||
triggers.add(trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,12 +117,12 @@ public class GenericEventDetector {
|
||||
return receiver.toString() + "." + methodName;
|
||||
}
|
||||
|
||||
private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
||||
private List<TriggerPoint> buildTriggerPoints(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
||||
String eventValue = null;
|
||||
if (forcedEvent != null) {
|
||||
eventValue = context.resolveString(forcedEvent);
|
||||
} else {
|
||||
if (node.arguments().isEmpty()) return null;
|
||||
if (node.arguments().isEmpty()) return Collections.emptyList();
|
||||
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||
|
||||
// Try MessageBuilder extraction first
|
||||
@@ -123,18 +138,37 @@ public class GenericEventDetector {
|
||||
MethodDeclaration method = findEnclosingMethod(node);
|
||||
TypeDeclaration type = findEnclosingType(node);
|
||||
|
||||
if (type == null) return null;
|
||||
if (type == null) return Collections.emptyList();
|
||||
|
||||
String sourceState = extractSourceState(node);
|
||||
|
||||
List<TriggerPoint> results = new ArrayList<>();
|
||||
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||
String[] parts = eventValue.substring(9).split(",");
|
||||
for (String part : parts) {
|
||||
if (!part.trim().isEmpty()) {
|
||||
results.add(TriggerPoint.builder()
|
||||
.event(part.trim())
|
||||
.sourceState(sourceState)
|
||||
.className(context.getFqn(type))
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results.add(TriggerPoint.builder()
|
||||
.event(eventValue)
|
||||
.sourceState(sourceState)
|
||||
.className(context.getFqn(type))
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||
.build());
|
||||
}
|
||||
|
||||
return TriggerPoint.builder()
|
||||
.event(eventValue)
|
||||
.sourceState(sourceState)
|
||||
.className(context.getFqn(type))
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||
.build();
|
||||
return results;
|
||||
}
|
||||
|
||||
private String extractSourceState(ASTNode node) {
|
||||
@@ -268,7 +302,8 @@ public class GenericEventDetector {
|
||||
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
||||
// If the event is obtained by calling a method on a provider/supplier parameter,
|
||||
// return the provider's name so CallGraphBuilder can trace the lambda argument
|
||||
return sn.getIdentifier();
|
||||
String traced = extractEventFromMessageBuilder(sn);
|
||||
return traced != null ? traced : sn.getIdentifier();
|
||||
}
|
||||
|
||||
Expression receiver = current.getExpression();
|
||||
|
||||
@@ -24,6 +24,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
private final CallGraphBuilder callGraphBuilder;
|
||||
private final LifecycleDetector lifecycleDetector;
|
||||
private final InterceptorDetector interceptorDetector;
|
||||
private final SpringComponentDetector componentDetector;
|
||||
|
||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||
this.context = context;
|
||||
@@ -34,6 +35,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
this.callGraphBuilder = new CallGraphBuilder(context);
|
||||
this.lifecycleDetector = new LifecycleDetector(context);
|
||||
this.interceptorDetector = new InterceptorDetector(context);
|
||||
this.componentDetector = new SpringComponentDetector(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,6 +60,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
allEntryPoints.addAll(mvcDetector.detect(cu));
|
||||
allEntryPoints.addAll(messagingDetector.detect(cu));
|
||||
allEntryPoints.addAll(interceptorDetector.detect(cu));
|
||||
allEntryPoints.addAll(componentDetector.detect(cu));
|
||||
}
|
||||
log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size());
|
||||
return allEntryPoints;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.Annotation;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.MemberValuePair;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.NormalAnnotation;
|
||||
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class SpringComponentDetector {
|
||||
private final CodebaseContext context;
|
||||
|
||||
public List<EntryPoint> detect(CompilationUnit cu) {
|
||||
List<EntryPoint> entryPoints = new ArrayList<>();
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
if (!(node.getParent() instanceof TypeDeclaration)) {
|
||||
return super.visit(node);
|
||||
}
|
||||
TypeDeclaration parentTd = (TypeDeclaration) node.getParent();
|
||||
|
||||
for (Object modifier : node.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation) {
|
||||
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if (typeName.endsWith("Scheduled")) {
|
||||
Map<String, String> meta = new HashMap<>();
|
||||
String cron = extractAnnotationValue(annotation, "cron");
|
||||
if (!cron.isEmpty())
|
||||
meta.put("cron", cron);
|
||||
String fixedRate = extractAnnotationValue(annotation, "fixedRate");
|
||||
if (!fixedRate.isEmpty())
|
||||
meta.put("fixedRate", fixedRate);
|
||||
String fixedDelay = extractAnnotationValue(annotation, "fixedDelay");
|
||||
if (!fixedDelay.isEmpty())
|
||||
meta.put("fixedDelay", fixedDelay);
|
||||
|
||||
entryPoints.add(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.name("@Scheduled: " + node.getName().getIdentifier())
|
||||
.className(context.getFqn(parentTd))
|
||||
.methodName(node.getName().getIdentifier())
|
||||
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||
.metadata(meta)
|
||||
.build());
|
||||
} else if (typeName.endsWith("EventListener")) {
|
||||
Map<String, String> meta = new HashMap<>();
|
||||
String classes = extractAnnotationValue(annotation, "classes");
|
||||
if (!classes.isEmpty())
|
||||
meta.put("classes", classes);
|
||||
String condition = extractAnnotationValue(annotation, "condition");
|
||||
if (!condition.isEmpty())
|
||||
meta.put("condition", condition);
|
||||
|
||||
entryPoints.add(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.name("@EventListener: " + node.getName().getIdentifier())
|
||||
.className(context.getFqn(parentTd))
|
||||
.methodName(node.getName().getIdentifier())
|
||||
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||
.metadata(meta)
|
||||
.build());
|
||||
} else if (typeName.endsWith("Around") || typeName.endsWith("Before") || typeName.endsWith("After") || typeName.endsWith("AfterReturning") || typeName.endsWith("AfterThrowing")) {
|
||||
Map<String, String> meta = new HashMap<>();
|
||||
String value = extractAnnotationValue(annotation, "value");
|
||||
if (!value.isEmpty())
|
||||
meta.put("pointcut", value);
|
||||
else {
|
||||
String pointcut = extractAnnotationValue(annotation, "pointcut");
|
||||
if (!pointcut.isEmpty())
|
||||
meta.put("pointcut", pointcut);
|
||||
}
|
||||
|
||||
entryPoints.add(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.name("@" + annotation.getTypeName().getFullyQualifiedName() + ": " + node.getName().getIdentifier())
|
||||
.className(context.getFqn(parentTd))
|
||||
.methodName(node.getName().getIdentifier())
|
||||
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||
.metadata(meta)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
private String extractAnnotationValue(Annotation annotation, String memberName) {
|
||||
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||
if ("value".equals(memberName)) {
|
||||
return sma.getValue().toString();
|
||||
}
|
||||
} else if (annotation instanceof NormalAnnotation na) {
|
||||
for (Object pairObj : na.values()) {
|
||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||
if (pair.getName().getIdentifier().equals(memberName)) {
|
||||
return pair.getValue().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@ public class CodebaseContext {
|
||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||
|
||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||
private final Map<String, List<String>> enumValues = new HashMap<>();
|
||||
|
||||
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
|
||||
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
|
||||
@@ -188,6 +189,8 @@ public class CodebaseContext {
|
||||
for (Object type : cu.types()) {
|
||||
if (type instanceof TypeDeclaration td) {
|
||||
indexType(td, packageName, javaFile);
|
||||
} else if (type instanceof EnumDeclaration ed) {
|
||||
indexEnum(ed, packageName, javaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,9 +213,18 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
// Inheritance Mapping
|
||||
for (Object itf : td.superInterfaceTypes()) {
|
||||
String itfName = itf.toString();
|
||||
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
||||
// Track implementations (for both interfaces and superclasses)
|
||||
if (td.superInterfaceTypes() != null) {
|
||||
for (Object itfObj : td.superInterfaceTypes()) {
|
||||
Type itf = (Type) itfObj;
|
||||
String itfName = extractTypeName(itf);
|
||||
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
||||
}
|
||||
}
|
||||
Type superclass = td.getSuperclassType();
|
||||
if (superclass != null) {
|
||||
String superName = extractTypeName(superclass);
|
||||
interfaceToImpls.computeIfAbsent(superName, k -> new ArrayList<>()).add(fqn);
|
||||
}
|
||||
|
||||
// Recursively index nested types
|
||||
@@ -224,28 +236,67 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public List<String> getImplementations(String interfaceName) {
|
||||
Set<String> allImpls = new HashSet<>();
|
||||
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
||||
return new ArrayList<>(allImpls);
|
||||
}
|
||||
|
||||
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||
if (!visited.add(typeName)) return;
|
||||
|
||||
// Try direct match
|
||||
List<String> impls = interfaceToImpls.get(interfaceName);
|
||||
if (impls != null) return impls;
|
||||
List<String> directImpls = interfaceToImpls.get(typeName);
|
||||
|
||||
// Try FQN match if input was simple name
|
||||
String fqn = simpleNameToFqn.get(interfaceName);
|
||||
if (fqn != null && interfaceToImpls.containsKey(fqn)) {
|
||||
return interfaceToImpls.get(fqn);
|
||||
}
|
||||
|
||||
// Try simple name match if input was FQN
|
||||
if (interfaceName.contains(".")) {
|
||||
String simpleName = interfaceName.substring(interfaceName.lastIndexOf('.') + 1);
|
||||
List<String> simpleImpls = interfaceToImpls.get(simpleName);
|
||||
if (simpleImpls != null) {
|
||||
// To be perfectly safe, we could check if simpleNameToFqn matches,
|
||||
// but for call graphs, returning potential implementations is fine.
|
||||
return simpleImpls;
|
||||
if (directImpls == null) {
|
||||
String fqn = simpleNameToFqn.get(typeName);
|
||||
if (fqn != null) {
|
||||
directImpls = interfaceToImpls.get(fqn);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
// Try simple name match if input was FQN
|
||||
if (directImpls == null && typeName.contains(".")) {
|
||||
String simpleName = typeName.substring(typeName.lastIndexOf('.') + 1);
|
||||
directImpls = interfaceToImpls.get(simpleName);
|
||||
}
|
||||
|
||||
if (directImpls != null) {
|
||||
for (String impl : directImpls) {
|
||||
results.add(impl);
|
||||
// Recursively find implementations of the implementation (e.g., subclasses of an abstract class)
|
||||
collectImplementations(impl, results, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
|
||||
String simpleName = ed.getName().getIdentifier();
|
||||
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||
|
||||
classes.put(fqn, (CompilationUnit) ed.getRoot());
|
||||
classPaths.put(fqn, javaFile);
|
||||
|
||||
List<String> values = new ArrayList<>();
|
||||
for (Object o : ed.enumConstants()) {
|
||||
if (o instanceof EnumConstantDeclaration ecd) {
|
||||
values.add(fqn + "." + ecd.getName().getIdentifier());
|
||||
}
|
||||
}
|
||||
enumValues.put(fqn, values);
|
||||
if (!simpleNameToFqn.containsKey(simpleName)) {
|
||||
simpleNameToFqn.put(simpleName, fqn);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||
if (fqnOrSimpleName == null) return null;
|
||||
List<String> values = enumValues.get(fqnOrSimpleName);
|
||||
if (values != null) return values;
|
||||
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
|
||||
if (fqn != null) return enumValues.get(fqn);
|
||||
// Also try stripping array or generic types if any (e.g. MyEnum[])
|
||||
return null;
|
||||
}
|
||||
|
||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||
|
||||
@@ -54,11 +54,19 @@ public class ExporterCommand implements Callable<Integer> {
|
||||
@Option(names = {"--state"}, description = "Format for states when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
|
||||
private click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat;
|
||||
|
||||
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||
private boolean debug;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
var out = spec.commandLine().getOut();
|
||||
var err = spec.commandLine().getErr();
|
||||
|
||||
if (debug) {
|
||||
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
|
||||
}
|
||||
|
||||
if (inputDir == null && jsonFile == null) {
|
||||
inputDir = Path.of(".");
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ import java.util.List;
|
||||
public class GoldenUpdater {
|
||||
|
||||
private static final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||
private static final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
|
||||
private static final ExportService exportService = new ExportService(exporters, enrichmentService);
|
||||
private static final ExportService exportService = new ExportService(exporters);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
List<TestScenario> scenarios = provideTestScenarios();
|
||||
|
||||
@@ -137,6 +137,12 @@ public class RegressionTest {
|
||||
root.resolve("state_machines/maven_multi_module/core-module"),
|
||||
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
||||
"MavenOrderStateMachine"
|
||||
),
|
||||
new TestScenario(
|
||||
"Complex Multi-Module Sample",
|
||||
root.resolve("state_machines/complex_multi_module_sm"),
|
||||
Path.of("src/test/resources/golden/StateMachineConfig"),
|
||||
"StateMachineConfig"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,4 +118,78 @@ class TransitionLinkerEnricherTest {
|
||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChain.getMatchedTransitions()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWildcardVariable() {
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||
t1.setEvent(Event.of("PAY", "PAY"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("event").build())
|
||||
.contextMachineId("testMachine")
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("testMachine")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(result, null, null);
|
||||
|
||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void shouldNotMatchArbitraryGetXMethodWildcard() {
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||
t1.setEvent(Event.of("PAY", "PAY"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("richEvent.getId()").build())
|
||||
.contextMachineId("testMachine")
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("testMachine")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(result, null, null);
|
||||
|
||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChain.getMatchedTransitions()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowWildcardWhenContextMachineIdIsMissing() {
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||
t1.setEvent(Event.of("PAY", "PAY"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("event").build())
|
||||
// NO contextMachineId or stateMachineId
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("testMachine")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(result, null, null);
|
||||
|
||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PropertyResolverTest {
|
||||
|
||||
@Test
|
||||
void testYamlPropertiesAreLoadedAndFlattened(@TempDir Path tempDir) throws IOException {
|
||||
Path applicationYml = tempDir.resolve("application.yml");
|
||||
Files.writeString(applicationYml, """
|
||||
messaging:
|
||||
sqs:
|
||||
integration-system1:
|
||||
callback:
|
||||
queue: "integration-system1-queue"
|
||||
""");
|
||||
|
||||
PropertyResolver resolver = new PropertyResolver();
|
||||
Map<String, String> props = resolver.resolveProperties(tempDir);
|
||||
|
||||
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPropertiesFilesAreLoaded(@TempDir Path tempDir) throws IOException {
|
||||
Path applicationProps = tempDir.resolve("application.properties");
|
||||
Files.writeString(applicationProps, "messaging.sqs.integration-system1.callback.queue=integration-system1-queue-props");
|
||||
|
||||
PropertyResolver resolver = new PropertyResolver();
|
||||
Map<String, String> props = resolver.resolveProperties(tempDir);
|
||||
|
||||
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue-props");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlaceholderResolution() {
|
||||
PropertyResolver resolver = new PropertyResolver();
|
||||
Map<String, String> properties = Map.of("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
|
||||
|
||||
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.integration-system1.callback.queue}", properties);
|
||||
assertThat(resolved).isEqualTo("SQS: integration-system1-queue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlaceholderResolutionWithDefault() {
|
||||
PropertyResolver resolver = new PropertyResolver();
|
||||
Map<String, String> properties = Map.of();
|
||||
|
||||
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.queue:default-queue}", properties);
|
||||
assertThat(resolved).isEqualTo("SQS: default-queue");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,4 +44,71 @@ public class GenericEventDetectorTest {
|
||||
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
||||
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMessageBuilderIntermediateVariable(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("MyService.java"),
|
||||
"package com.example;\n" +
|
||||
"import org.springframework.messaging.support.MessageBuilder;\n" +
|
||||
"import org.springframework.messaging.Message;\n" +
|
||||
"import org.springframework.statemachine.StateMachine;\n" +
|
||||
"public class MyService {\n" +
|
||||
" private StateMachine<String, String> stateMachine;\n" +
|
||||
" private void a(MyInterface myEvent) {\n" +
|
||||
" MessageBuilder<MyInterface> messageBuilder = MessageBuilder.withPayload(myEvent).setHeader(\"k\", \"v\");\n" +
|
||||
" stateMachine.sendEvent(messageBuilder.build());\n" +
|
||||
" }\n" +
|
||||
"}\n");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
|
||||
CompilationUnit cu = context.getCompilationUnits().iterator().next();
|
||||
List<TriggerPoint> triggers = detector.detect(cu);
|
||||
|
||||
assertThat(triggers).hasSize(1);
|
||||
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
||||
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("MyEnum.java"),
|
||||
"package com.example;\n" +
|
||||
"public enum MyEnum {\n" +
|
||||
" STATE_A, STATE_B;\n" +
|
||||
"}\n");
|
||||
|
||||
Files.writeString(dir.resolve("MyService.java"),
|
||||
"package com.example;\n" +
|
||||
"import org.springframework.statemachine.StateMachine;\n" +
|
||||
"public class MyService {\n" +
|
||||
" private StateMachine<String, String> stateMachine;\n" +
|
||||
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
|
||||
" public void trigger() {\n" +
|
||||
" stateMachine.sendEvent(this.getEvent());\n" +
|
||||
" }\n" +
|
||||
"}\n");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
|
||||
|
||||
CompilationUnit serviceCu = context.getCompilationUnits().stream()
|
||||
.filter(cu -> cu.getJavaElement() == null || cu.getJavaElement().getElementName().contains("MyService"))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
List<TriggerPoint> triggers = detector.detect(serviceCu);
|
||||
|
||||
System.out.println("TRIGGERS: " + triggers);
|
||||
assertThat(triggers).hasSize(2);
|
||||
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_A"));
|
||||
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_B"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package click.kamil.springstatemachineexporter.exporter;
|
||||
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||
import click.kamil.springstatemachineexporter.model.Guard;
|
||||
import click.kamil.springstatemachineexporter.model.Action;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlantUmlTest {
|
||||
|
||||
@Test
|
||||
void testExportSanitizesLabels() {
|
||||
PlantUml plantUml = new PlantUml();
|
||||
ExportOptions options = ExportOptions.builder()
|
||||
.embedIdentifiers(true)
|
||||
.useLambdaGuards(false)
|
||||
.build();
|
||||
|
||||
State state1 = State.of("State1");
|
||||
State state2 = State.of("State2");
|
||||
|
||||
Transition t = new Transition();
|
||||
t.setType(TransitionType.EXTERNAL);
|
||||
t.setSourceStates(List.of(state1));
|
||||
t.setTargetStates(List.of(state2));
|
||||
|
||||
// Add a guard and action containing problematic characters
|
||||
t.setGuard(Guard.of("list.size() < 5", false, null, null, null, null));
|
||||
t.setActions(List.of(Action.of("doSomething(new int[]{1, 2})", false, null, null, null, null)));
|
||||
|
||||
String result = plantUml.export(
|
||||
"TestMachine",
|
||||
List.of(t),
|
||||
Set.of("State1"),
|
||||
Set.of("State2"),
|
||||
options
|
||||
);
|
||||
|
||||
// Verify that < and > are replaced with HTML entities
|
||||
assertThat(result).doesNotContain("< 5");
|
||||
assertThat(result).contains("< 5");
|
||||
|
||||
// Verify that [ and ] are replaced with HTML entities inside the link
|
||||
assertThat(result).doesNotContain("[list.size() < 5]");
|
||||
assertThat(result).doesNotContain("new int[]{1, 2}");
|
||||
assertThat(result).contains("[list.size() < 5]");
|
||||
assertThat(result).contains("new int[]{1, 2}");
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "PLACE_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
@@ -17,7 +18,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "CANCEL_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
@@ -26,7 +28,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "PAY_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||
@@ -35,7 +38,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "SHIP_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||
@@ -44,7 +48,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "RETURN_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||
@@ -53,7 +58,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -185,7 +191,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -219,7 +226,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -248,10 +256,11 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ ]
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
@@ -278,7 +287,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -312,7 +322,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -346,7 +357,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 34
|
||||
"lineNumber" : 34,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
||||
@@ -17,7 +18,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "EXTERNAL_TRIGGER",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -26,7 +28,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 19
|
||||
"lineNumber" : 19,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "SUBMIT_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -35,7 +38,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 20
|
||||
"lineNumber" : 20,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "CANCEL_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -44,7 +48,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "[LIFECYCLE:RESTORE]",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -53,7 +58,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 29
|
||||
"lineNumber" : 29,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "REACTIVE_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||
@@ -62,7 +68,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -160,7 +167,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 19
|
||||
"lineNumber" : 19,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -190,7 +198,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 20
|
||||
"lineNumber" : 20,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -220,7 +229,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -250,7 +260,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 34
|
||||
"lineNumber" : 34,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -284,10 +295,11 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 29
|
||||
"lineNumber" : 29,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : "orderId",
|
||||
"matchedTransitions" : [ ]
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
@@ -314,7 +326,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -343,7 +356,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 34
|
||||
"lineNumber" : 34,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
||||
@@ -17,7 +18,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "EXTERNAL_TRIGGER",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -26,7 +28,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 19
|
||||
"lineNumber" : 19,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "SUBMIT_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -35,7 +38,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 20
|
||||
"lineNumber" : 20,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "CANCEL_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -44,7 +48,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "[LIFECYCLE:RESTORE]",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
@@ -53,7 +58,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 29
|
||||
"lineNumber" : 29,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "REACTIVE_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||
@@ -62,7 +68,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -160,7 +167,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 19
|
||||
"lineNumber" : 19,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -190,7 +198,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 20
|
||||
"lineNumber" : 20,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -220,7 +229,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -250,7 +260,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 34
|
||||
"lineNumber" : 34,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -284,10 +295,11 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 29
|
||||
"lineNumber" : 29,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : "orderId",
|
||||
"matchedTransitions" : [ ]
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
@@ -314,7 +326,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -343,7 +356,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -55,7 +56,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 40
|
||||
"lineNumber" : 40,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "ORDER_EVENT",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
||||
@@ -17,7 +18,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 52
|
||||
"lineNumber" : 52,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -103,7 +105,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 40
|
||||
"lineNumber" : 40,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -67,7 +68,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
digraph statemachine {
|
||||
rankdir=LR;
|
||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||
edge [fontname="Arial", fontsize=10];
|
||||
|
||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||
_start -> NEW;
|
||||
COMPLETED [fillcolor=lightgray];
|
||||
CANCELLED [fillcolor=lightgray];
|
||||
NEW -> PROCESSING [label="OrderEvent.PROCESS", style="solid", color="black"];
|
||||
PROCESSING -> COMPLETED [label="OrderEvent.COMPLETE", style="solid", color="black"];
|
||||
NEW -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
|
||||
PROCESSING -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "eventProvider",
|
||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||
@@ -17,7 +18,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 50
|
||||
"lineNumber" : 50,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "customMessage",
|
||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||
@@ -26,7 +28,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 69
|
||||
"lineNumber" : 78,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -61,6 +64,17 @@
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/functional-complete",
|
||||
"className" : "click.kamil.web.OrderController",
|
||||
"methodName" : "functionalCompleteOrder",
|
||||
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/functional-complete",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "RABBIT",
|
||||
"name" : "RABBIT: order.custom.transition.queue",
|
||||
@@ -129,7 +143,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -159,7 +174,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -189,7 +205,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -201,6 +218,37 @@
|
||||
"targetState" : "OrderState.CANCELLED",
|
||||
"event" : "OrderEvent.CANCEL"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/functional-complete",
|
||||
"className" : "click.kamil.web.OrderController",
|
||||
"methodName" : "functionalCompleteOrder",
|
||||
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/functional-complete",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.web.OrderController.functionalCompleteOrder", "click.kamil.web.OrderController.triggerTransitionFunction", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "OrderEvent.COMPLETE",
|
||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||
"methodName" : "sendMessage",
|
||||
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderState.PROCESSING",
|
||||
"targetState" : "OrderState.COMPLETED",
|
||||
"event" : "OrderEvent.COMPLETE"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "RABBIT",
|
||||
@@ -227,7 +275,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 69
|
||||
"lineNumber" : 78,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -261,7 +310,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -295,7 +345,8 @@
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 50
|
||||
"lineNumber" : 50,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -0,0 +1,35 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
set separator none
|
||||
hide empty description
|
||||
hide stereotype
|
||||
skinparam state {
|
||||
BackgroundColor white
|
||||
BorderColor #94a3b8
|
||||
BorderThickness 1
|
||||
FontName Inter
|
||||
FontSize 9
|
||||
FontStyle bold
|
||||
RoundCorner 20
|
||||
Padding 1
|
||||
}
|
||||
skinparam shadowing false
|
||||
skinparam ArrowFontName JetBrains Mono
|
||||
skinparam ArrowFontSize 8
|
||||
skinparam ArrowColor #cbd5e1
|
||||
skinparam ArrowThickness 1
|
||||
skinparam dpi 110
|
||||
skinparam svgLinkTarget _self
|
||||
|
||||
[*] --> OrderState.NEW
|
||||
|
||||
|
||||
OrderState.NEW -[#1E90FF,bold]-> OrderState.PROCESSING <<external>> : OrderEvent.PROCESS
|
||||
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.COMPLETED <<external>> : OrderEvent.COMPLETE
|
||||
OrderState.NEW -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
|
||||
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
|
||||
|
||||
OrderState.COMPLETED --> [*]
|
||||
OrderState.CANCELLED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
|
||||
<state id="NEW">
|
||||
<transition target="PROCESSING" event="OrderEvent.PROCESS"/>
|
||||
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
|
||||
</state>
|
||||
<state id="PROCESSING">
|
||||
<transition target="COMPLETED" event="OrderEvent.COMPLETE"/>
|
||||
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
|
||||
</state>
|
||||
<state id="COMPLETED">
|
||||
</state>
|
||||
<state id="CANCELLED">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -13,6 +13,14 @@ import java.util.List;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// Enable diagnostic mode early before SLF4J initializes
|
||||
for (String arg : args) {
|
||||
if ("--debug".equals(arg)) {
|
||||
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wiring including the specialized HTML exporter
|
||||
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter(), new HtmlExporter());
|
||||
var exportService = new ExportService(exporters);
|
||||
|
||||
@@ -59,11 +59,19 @@ public class HtmlExporterCommand implements Callable<Integer> {
|
||||
@Option(names = {"--no-metadata-pane"}, description = "Disable rendering the left metadata pane (entry points, flows) in the HTML viewer.")
|
||||
private boolean noMetadataPane;
|
||||
|
||||
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||
private boolean debug;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
var out = spec.commandLine().getOut();
|
||||
var err = spec.commandLine().getErr();
|
||||
|
||||
if (debug) {
|
||||
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
|
||||
}
|
||||
|
||||
if (inputDir == null && jsonFile == null) {
|
||||
inputDir = Path.of(".");
|
||||
}
|
||||
|
||||
@@ -238,9 +238,26 @@
|
||||
.tooltip-content {
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Tooltips */
|
||||
.tooltip-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.tooltip-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.tooltip-content::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tooltip-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.payload-box {
|
||||
background: #0f172a;
|
||||
|
||||
@@ -81,13 +81,6 @@ public class HtmlExporterTest {
|
||||
|
||||
// 7. Assertions - Surgical SVG Identification
|
||||
// Every event should be wrapped in an identifying hyperlink for robust JS selection
|
||||
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_html.html"), html);
|
||||
|
||||
click.kamil.springstatemachineexporter.exporter.ExportOptions plantUmlOptions = click.kamil.springstatemachineexporter.exporter.ExportOptions.builder()
|
||||
.embedIdentifiers(true)
|
||||
.build();
|
||||
click.kamil.springstatemachineexporter.exporter.PlantUml pumlExporter = new click.kamil.springstatemachineexporter.exporter.PlantUml();
|
||||
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_puml.puml"), pumlExporter.export(result, plantUmlOptions));
|
||||
|
||||
assertThat(html).contains("__CREATE");
|
||||
assertThat(html).contains("__ASSIGN");
|
||||
|
||||
@@ -14,6 +14,10 @@ public interface StateMachineService {
|
||||
// Quirky pattern 2: Generic Varargs
|
||||
<T extends IEvent> void sendEventsVarargs(T... events);
|
||||
|
||||
// Functional quirks: passing functions and method references
|
||||
<T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
|
||||
java.util.function.Function<T, Void> transitionAction);
|
||||
|
||||
// Child class of Spring's GenericMessage with a generic payload
|
||||
<T extends click.kamil.domain.OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage);
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ public class StateMachineServiceImpl implements StateMachineService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
|
||||
java.util.function.Function<T, Void> transitionAction) {
|
||||
// Resolve the event from the getter / supplier function
|
||||
T event = eventSupplier.get();
|
||||
// Apply the transition via the provided function / method reference
|
||||
transitionAction.apply(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage) {
|
||||
// Because CustomStateMachineMessage extends GenericMessage<T> and implements Message<T>,
|
||||
|
||||
@@ -34,4 +34,27 @@ public class OrderController {
|
||||
stateMachineService.sendMessage(OrderEvent.CANCEL);
|
||||
return "Order cancel event sent";
|
||||
}
|
||||
|
||||
// --- Functional Quirks for Static Analysis ---
|
||||
|
||||
// A getter returning a specific ENUM constant
|
||||
public OrderEvent getCompleteEvent() {
|
||||
return OrderEvent.COMPLETE;
|
||||
}
|
||||
|
||||
// A specific function that performs the transition
|
||||
public Void triggerTransitionFunction(click.kamil.domain.IEvent event) {
|
||||
stateMachineService.sendMessage(event);
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/functional-complete")
|
||||
public String functionalCompleteOrder() {
|
||||
// Pass the getter and the transition applier as method references
|
||||
stateMachineService.executeFunctionalTransition(
|
||||
this::getCompleteEvent,
|
||||
this::triggerTransitionFunction
|
||||
);
|
||||
return "Functional complete event triggered via method references";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user