Compare commits
4 Commits
411b7a0e52
...
dd0363cf64
| Author | SHA1 | Date | |
|---|---|---|---|
| dd0363cf64 | |||
| cc2cf1845b | |||
| 8d954804b2 | |||
| 66aaa9563f |
@@ -1,6 +1,5 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.MachineScopeFilter;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
|
||||
@@ -22,12 +22,14 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
||||
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
if (result.getMetadata() == null || context == null) {
|
||||
if (result.getMetadata() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
StateMachineTypeResolver.MachineTypes machineTypes = resolveMachineTypes(result, context);
|
||||
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TriggerPoint> triggers = result.getMetadata().getTriggers();
|
||||
List<TriggerPoint> canonicalTriggers = triggers == null ? null : triggers.stream()
|
||||
@@ -60,4 +62,18 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
||||
.properties(result.getMetadata().getProperties())
|
||||
.build());
|
||||
}
|
||||
|
||||
private static StateMachineTypeResolver.MachineTypes resolveMachineTypes(
|
||||
AnalysisResult result, CodebaseContext context) {
|
||||
StateMachineTypeResolver.MachineTypes resolved =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
if (resolved.stateTypeFqn() != null || resolved.eventTypeFqn() != null) {
|
||||
return resolved;
|
||||
}
|
||||
if (result.getStateTypeFqn() != null || result.getEventTypeFqn() != null) {
|
||||
return new StateMachineTypeResolver.MachineTypes(
|
||||
result.getStateTypeFqn(), result.getEventTypeFqn());
|
||||
}
|
||||
return StateMachineTypeResolver.MachineTypes.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
/**
|
||||
* Shared JavaBean accessor naming helpers used by analysis and dataflow code.
|
||||
*/
|
||||
public final class AccessorNaming {
|
||||
|
||||
private AccessorNaming() {
|
||||
}
|
||||
|
||||
public static boolean isBeanStyleAccessorName(String accessorName) {
|
||||
return (accessorName.startsWith("get") && accessorName.length() > 3)
|
||||
|| (accessorName.startsWith("is") && accessorName.length() > 2)
|
||||
|| (accessorName.startsWith("set") && accessorName.length() > 3);
|
||||
}
|
||||
|
||||
public static String propertyNameFromAccessor(String accessorName) {
|
||||
if (accessorName.startsWith("get") && accessorName.length() > 3) {
|
||||
return TrivialAccessorDetector.decapitalize(accessorName.substring(3));
|
||||
}
|
||||
if (accessorName.startsWith("is") && accessorName.length() > 2) {
|
||||
return TrivialAccessorDetector.decapitalize(accessorName.substring(2));
|
||||
}
|
||||
if (accessorName.startsWith("set") && accessorName.length() > 3) {
|
||||
return TrivialAccessorDetector.decapitalize(accessorName.substring(3));
|
||||
}
|
||||
return accessorName.startsWith("get") ? accessorName.substring(3) : accessorName;
|
||||
}
|
||||
|
||||
public static String setterMethodNameForGetter(String getterName) {
|
||||
return "set" + capitalizeProperty(propertyNameFromAccessor(getterName));
|
||||
}
|
||||
|
||||
public static String capitalizeProperty(String propertyName) {
|
||||
if (propertyName == null || propertyName.isEmpty()) {
|
||||
return propertyName;
|
||||
}
|
||||
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
|
||||
return propertyName;
|
||||
}
|
||||
return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
|
||||
}
|
||||
|
||||
public static boolean looksLikeIndexedAccessorName(String methodName) {
|
||||
return methodName.startsWith("get") || methodName.startsWith("is") || methodName.startsWith("set");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
@@ -117,6 +118,17 @@ public class AnalysisResult {
|
||||
.properties(metadata.getProperties())
|
||||
.build();
|
||||
}
|
||||
|
||||
canonicalizeResolvedIdentifiers();
|
||||
}
|
||||
|
||||
private void canonicalizeResolvedIdentifiers() {
|
||||
if (stateTypeFqn == null && eventTypeFqn == null) {
|
||||
return;
|
||||
}
|
||||
click.kamil.springstatemachineexporter.analysis.service.AnalysisResultFinalizer.applyCanonicalization(
|
||||
this,
|
||||
new StateMachineTypeResolver.MachineTypes(stateTypeFqn, eventTypeFqn));
|
||||
}
|
||||
|
||||
private static TriggerPoint resolveTrigger(
|
||||
|
||||
@@ -4,6 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.GetterChainEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.GetterBodyConstantScanner;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
@@ -400,34 +401,8 @@ public final class AccessorResolver {
|
||||
}
|
||||
|
||||
private List<String> extractConstantGetterReturn(String ownerFqn, String methodName) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> constants = new ArrayList<>();
|
||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> extractor =
|
||||
constantExtractor != null
|
||||
? constantExtractor::extractConstantsFromExpression
|
||||
: (expr, out) -> {
|
||||
String resolved = constantResolver.resolve(expr, context);
|
||||
if (resolved != null) {
|
||||
out.add(resolved);
|
||||
}
|
||||
};
|
||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
extractor.accept(node.getExpression(), constants);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return constants;
|
||||
return GetterBodyConstantScanner.extractConstantGetterReturn(
|
||||
context, constantExtractor, constantResolver, ownerFqn, methodName);
|
||||
}
|
||||
|
||||
private static String qualifyEnumLikeResult(MethodDeclaration getter, String result) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public final class ClassCompatibilityCache {
|
||||
|
||||
private final Map<Map.Entry<String, String>, Boolean> cache = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> memoCache = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean areClassesCompatible(String classNeighbor, String classTarget, ClassCompatibilityChecker checker) {
|
||||
if (classNeighbor == null || classTarget == null) {
|
||||
@@ -22,8 +23,18 @@ public final class ClassCompatibilityCache {
|
||||
return cache.computeIfAbsent(key, ignored -> checker.check(classNeighbor, classTarget));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T memoize(String key, java.util.function.Function<String, T> computer) {
|
||||
return (T) memoCache.computeIfAbsent(key, computer);
|
||||
}
|
||||
|
||||
public void clearMemoCache() {
|
||||
memoCache.clear();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
memoCache.clear();
|
||||
}
|
||||
|
||||
private static Map.Entry<String, String> cacheKey(String classNeighbor, String classTarget) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Scans trivial getter bodies for constant return values.
|
||||
*/
|
||||
public final class GetterBodyConstantScanner {
|
||||
|
||||
private GetterBodyConstantScanner() {
|
||||
}
|
||||
|
||||
public static List<String> extractConstantGetterReturn(
|
||||
CodebaseContext context,
|
||||
ConstantExtractor constantExtractor,
|
||||
ConstantResolver constantResolver,
|
||||
String ownerFqn,
|
||||
String methodName) {
|
||||
return extractConstantGetterReturn(context, constantExtractor, constantResolver, ownerFqn, methodName, null, null);
|
||||
}
|
||||
|
||||
public static List<String> extractConstantGetterReturn(
|
||||
CodebaseContext context,
|
||||
ConstantExtractor constantExtractor,
|
||||
ConstantResolver constantResolver,
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
CompilationUnit contextCu,
|
||||
Set<String> ignoredVisited) {
|
||||
TypeDeclaration typeDeclaration = contextCu != null
|
||||
? context.getTypeDeclaration(ownerFqn, contextCu)
|
||||
: null;
|
||||
if (typeDeclaration == null) {
|
||||
typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> constants = new ArrayList<>();
|
||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> extractor =
|
||||
constantExtractor != null
|
||||
? constantExtractor::extractConstantsFromExpression
|
||||
: (expr, out) -> {
|
||||
String resolved = constantResolver.resolve(expr, context);
|
||||
if (resolved != null) {
|
||||
out.add(resolved);
|
||||
}
|
||||
};
|
||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
extractor.accept(node.getExpression(), constants);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
@@ -1073,13 +1073,18 @@ public class ConstantResolver {
|
||||
if (ann instanceof SingleMemberAnnotation sma) {
|
||||
valueExpr = sma.getValue();
|
||||
} else if (ann instanceof NormalAnnotation na) {
|
||||
for (Object pairObj : na.values()) {
|
||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||
if ("value".equals(pair.getName().getIdentifier())) {
|
||||
valueExpr = pair.getValue();
|
||||
break;
|
||||
String raw = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(ann, "value");
|
||||
if (!raw.isEmpty()) {
|
||||
if (raw.startsWith("\"") && raw.endsWith("\"")) {
|
||||
raw = raw.substring(1, raw.length() - 1);
|
||||
}
|
||||
if (raw.contains("${")) {
|
||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
|
||||
raw, java.util.Collections.emptyMap());
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (valueExpr instanceof StringLiteral sl) {
|
||||
@@ -1099,11 +1104,7 @@ public class ConstantResolver {
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode current = node;
|
||||
while (current != null && !(current instanceof TypeDeclaration)) {
|
||||
current = current.getParent();
|
||||
}
|
||||
return (TypeDeclaration) current;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||
}
|
||||
|
||||
private MethodDeclaration findInvokedMethod(org.eclipse.jdt.core.dom.MethodInvocation mi, CodebaseContext context) {
|
||||
@@ -1125,14 +1126,7 @@ public class ConstantResolver {
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof MethodDeclaration md) {
|
||||
return md;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return null;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
}
|
||||
|
||||
private String resolveLocalType(SimpleName sn) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -55,9 +56,12 @@ public final class MachineEnumCanonicalizer {
|
||||
if (states == null || states.isEmpty()) {
|
||||
return states;
|
||||
}
|
||||
return states.stream()
|
||||
.map(state -> canonicalizeState(state, stateTypeFqn))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
LinkedHashMap<String, State> byFullIdentifier = new LinkedHashMap<>();
|
||||
for (State state : states) {
|
||||
State canonical = canonicalizeState(state, stateTypeFqn);
|
||||
byFullIdentifier.putIfAbsent(canonical.fullIdentifier(), canonical);
|
||||
}
|
||||
return new LinkedHashSet<>(byFullIdentifier.values());
|
||||
}
|
||||
|
||||
public static TriggerPoint canonicalizeTriggerPoint(
|
||||
@@ -166,11 +170,12 @@ public final class MachineEnumCanonicalizer {
|
||||
if (states == null) {
|
||||
return null;
|
||||
}
|
||||
List<State> canonical = new ArrayList<>(states.size());
|
||||
LinkedHashMap<String, State> byFullIdentifier = new LinkedHashMap<>();
|
||||
for (State state : states) {
|
||||
canonical.add(canonicalizeState(state, stateTypeFqn));
|
||||
State canonical = canonicalizeState(state, stateTypeFqn);
|
||||
byFullIdentifier.putIfAbsent(canonical.fullIdentifier(), canonical);
|
||||
}
|
||||
return canonical;
|
||||
return new ArrayList<>(byFullIdentifier.values());
|
||||
}
|
||||
|
||||
static Event canonicalizeEvent(Event event, String enumTypeFqn) {
|
||||
@@ -194,21 +199,21 @@ public final class MachineEnumCanonicalizer {
|
||||
if (state == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||
return state;
|
||||
}
|
||||
String raw = resolvePlaceholder(state.rawName());
|
||||
String full = resolvePlaceholder(
|
||||
state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName());
|
||||
String canonical = canonicalizeLabel(full, enumTypeFqn);
|
||||
if (canonical.equals(state.fullIdentifier()) && raw.equals(state.rawName())) {
|
||||
return state;
|
||||
}
|
||||
|
||||
String raw;
|
||||
if (isStringOrPrimitiveType(enumTypeFqn) && canonical.startsWith(enumTypeFqn + ".")) {
|
||||
String constant = canonical.substring(enumTypeFqn.length() + 1);
|
||||
raw = "\"" + constant + "\"";
|
||||
} else {
|
||||
String fnRaw = toFnForm(canonical, enumTypeFqn);
|
||||
if (fnRaw != null) {
|
||||
raw = fnRaw;
|
||||
}
|
||||
raw = fnRaw != null ? fnRaw : resolvePlaceholder(state.rawName());
|
||||
}
|
||||
|
||||
if (canonical.equals(state.fullIdentifier()) && raw.equals(state.rawName())) {
|
||||
return state;
|
||||
}
|
||||
return State.of(raw, canonical);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.service.TypeResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
@@ -204,51 +205,6 @@ public final class StateMachineTypeResolver {
|
||||
CompilationUnit cu,
|
||||
CodebaseContext context,
|
||||
Map<String, String> typeVarBindings) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
if (type instanceof WildcardType wildcardType) {
|
||||
Type bound = wildcardType.getBound();
|
||||
return bound != null ? resolveTypeToFqn(bound, cu, context, typeVarBindings) : null;
|
||||
}
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu, context, typeVarBindings);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
if (simpleName.contains("<")) {
|
||||
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
||||
}
|
||||
|
||||
if (typeVarBindings.containsKey(simpleName)) {
|
||||
return typeVarBindings.get(simpleName);
|
||||
}
|
||||
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
if (!packageName.isEmpty()) {
|
||||
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||
if (localTd != null) {
|
||||
return context.getFqn(localTd);
|
||||
}
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
return new TypeResolver(context).resolveTypeToFqn(type, cu, typeVarBindings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
protected final PathBindingEvaluator pathBindingEvaluator;
|
||||
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
||||
private final Map<String, List<String>> polymorphicCallCache = new HashMap<>();
|
||||
protected Map<String, List<CallEdge>> graph;
|
||||
|
||||
private ASTNode parseExpressionString(String expr) {
|
||||
if (expr == null) return null;
|
||||
@@ -834,7 +835,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (declaredType != null) {
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
List<String> values = constantExtractor.resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
|
||||
List<String> values = resolveAccessorConstants(declaredType, methodName, cu);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
polymorphicEvents.addAll(values);
|
||||
}
|
||||
@@ -870,7 +871,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||
if (currentTd != null && context.findMethodDeclaration(currentTd, methodName, true) != null) {
|
||||
CompilationUnit cu = currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
List<String> values = constantExtractor.resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu);
|
||||
List<String> values = resolveAccessorConstants(context.getFqn(currentTd), methodName, cu);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
polymorphicEvents.addAll(values);
|
||||
sourceMethod = methodFqn;
|
||||
@@ -905,7 +906,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
for (String type : typesToInspect) {
|
||||
Set<String> visited = new HashSet<>();
|
||||
TypeDeclaration baseTd = context.getTypeDeclaration(type);
|
||||
CompilationUnit cuToUse = null;
|
||||
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
|
||||
@@ -918,7 +918,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||
}
|
||||
}
|
||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse, ResolutionBudget.defaults(), false);
|
||||
List<String> constants = resolveAccessorConstants(type, methodName, cuToUse);
|
||||
for (String constant : constants) {
|
||||
if (!polymorphicEvents.contains(constant)) {
|
||||
polymorphicEvents.add(constant);
|
||||
@@ -1073,19 +1073,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
protected MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
}
|
||||
|
||||
protected TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||
}
|
||||
|
||||
protected List<String> resolveArguments(List<?> astArguments) {
|
||||
@@ -1321,33 +1313,24 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
protected 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();
|
||||
}
|
||||
return typeResolver.resolveTypeToFqn(type, contextNode);
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
protected List<String> resolveAccessorConstants(String ownerFqn, String methodName, CompilationUnit contextCu) {
|
||||
if (ownerFqn == null || methodName == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
TypeDeclaration ownerType = contextCu != null
|
||||
? context.getTypeDeclaration(ownerFqn, contextCu)
|
||||
: context.getTypeDeclaration(ownerFqn);
|
||||
if (ownerType == null) {
|
||||
ownerType = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
return accessorResolver.resolve(
|
||||
ownerFqn,
|
||||
methodName,
|
||||
new AccessorResolver.ReceiverContext(ownerType, null, contextCu, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
}
|
||||
|
||||
private String tryNarrowGetterViaLocalVarChain(MethodInvocation getterCall, SimpleName instanceSn) {
|
||||
@@ -1445,12 +1428,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);
|
||||
|
||||
MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
|
||||
if (getter == null || getter.getBody() == null) {
|
||||
return null;
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
context.getFqn(varTypeTd),
|
||||
getterName,
|
||||
new AccessorResolver.ReceiverContext(varTypeTd, cic, cu, false, fieldValues),
|
||||
ResolutionBudget.defaults());
|
||||
if (!resolved.isEmpty()) {
|
||||
return resolved.get(0);
|
||||
}
|
||||
|
||||
return constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
|
||||
return null;
|
||||
}
|
||||
|
||||
private void trackSetterCallsBetween(
|
||||
@@ -1778,7 +1764,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (!resolved.isEmpty()) {
|
||||
return resolved;
|
||||
}
|
||||
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), contextCu);
|
||||
}
|
||||
}
|
||||
} else if (exprNode instanceof MethodInvocation mi) {
|
||||
@@ -2255,9 +2240,195 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return keyToType;
|
||||
}
|
||||
|
||||
protected abstract Map<String, List<CallEdge>> buildCallGraph();
|
||||
protected abstract String callGraphCacheKey();
|
||||
|
||||
protected abstract String resolveCalledMethod(MethodInvocation node);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, List<CallEdge>> buildCallGraph() {
|
||||
Map<String, List<CallEdge>> cached =
|
||||
(Map<String, List<CallEdge>>) context.getCache().get(callGraphCacheKey());
|
||||
if (cached != null) {
|
||||
this.graph = cached;
|
||||
return cached;
|
||||
}
|
||||
graph = new HashMap<>();
|
||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
MethodDeclaration md = findEnclosingMethod(node);
|
||||
if (md != null) {
|
||||
TypeDeclaration td = findEnclosingType(md);
|
||||
if (td != null) {
|
||||
String callerFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.findConditionConstraint(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
String typeName = emr.getExpression().toString();
|
||||
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||
TypeDeclaration refType = findEnclosingType(node);
|
||||
if (refType != null) {
|
||||
String refMethod = resolveMethodInType(refType, emr.getName().getIdentifier());
|
||||
if (refMethod != null) {
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
CallEdge implEdge = new CallEdge(
|
||||
impl + "." + emr.getName().getIdentifier(), args, receiver);
|
||||
implEdge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(implEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
MethodDeclaration md = findEnclosingMethod(node);
|
||||
if (md != null) {
|
||||
TypeDeclaration tdOuter = findEnclosingType(md);
|
||||
if (tdOuter != null) {
|
||||
String callerFqn = 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());
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
context.getCache().put(callGraphCacheKey(), graph);
|
||||
return graph;
|
||||
}
|
||||
|
||||
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||
Expression receiver = mi.getExpression();
|
||||
String receiverType = null;
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(mi);
|
||||
if (td != null) {
|
||||
receiverType = context.getFqn(td);
|
||||
}
|
||||
} else if (receiver instanceof SimpleName sn) {
|
||||
receiverType = resolveReceiverTypeFallback(sn);
|
||||
} else if (receiver instanceof FieldAccess fa) {
|
||||
receiverType = resolveReceiverTypeFallback(fa.getName());
|
||||
} else if (receiver instanceof MethodInvocation innerMi) {
|
||||
receiverType = resolveMethodInvocationReturnType(innerMi);
|
||||
}
|
||||
|
||||
if (receiverType == null) {
|
||||
ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
|
||||
if (binding != null) {
|
||||
receiverType = binding.getErasure().getQualifiedName();
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverType != null) {
|
||||
String rawType = receiverType;
|
||||
if (rawType.contains("<")) {
|
||||
rawType = rawType.substring(0, rawType.indexOf('<'));
|
||||
}
|
||||
if (rawType.equals("java.util.Map") || rawType.equals("Map")) {
|
||||
String valType = resolveMapValueType(mi);
|
||||
if (valType != null) {
|
||||
return valType;
|
||||
}
|
||||
}
|
||||
if (receiverType.contains("<")) {
|
||||
receiverType = receiverType.substring(0, receiverType.indexOf('<'));
|
||||
}
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
TypeDeclaration td = context.getTypeDeclaration(receiverType);
|
||||
if (td == null && receiverType.contains(".")) {
|
||||
String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
|
||||
td = context.getTypeDeclaration(simpleClass);
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getReturnType2() != null) {
|
||||
return resolveTypeToFqn(md.getReturnType2(), mi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
||||
if (methodBinding != null && methodBinding.getReturnType() != null) {
|
||||
return methodBinding.getReturnType().getErasure().getQualifiedName();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
String baseCalled = resolveCalledMethod(node);
|
||||
if (baseCalled == null) return Collections.emptyList();
|
||||
@@ -2490,8 +2661,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
String ownerFqn = context.getFqn(td);
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit contextCu ? contextCu : null;
|
||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(
|
||||
ownerFqn, methodName, 0, new HashSet<>(), cu);
|
||||
List<String> constants = resolveAccessorConstants(ownerFqn, methodName, cu);
|
||||
if (constants.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,23 @@ public final class AnalysisResultFinalizer {
|
||||
result.setEventTypeFqn(machineTypes.eventTypeFqn());
|
||||
}
|
||||
|
||||
private static void applyCanonicalization(
|
||||
/**
|
||||
* Canonicalizes transitions, states, triggers, and matched transitions without validation.
|
||||
* Safe to call before transition relinking when machine types are known.
|
||||
*/
|
||||
public static void applyCanonicalization(
|
||||
AnalysisResult result,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
if (result == null || machineTypes == null) {
|
||||
return;
|
||||
}
|
||||
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
|
||||
return;
|
||||
}
|
||||
applyCanonicalizationFields(result, machineTypes);
|
||||
}
|
||||
|
||||
private static void applyCanonicalizationFields(
|
||||
AnalysisResult result,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||
MachineEnumCanonicalizer.canonicalizeTransitions(result.getTransitions(), machineTypes);
|
||||
|
||||
@@ -1,780 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class CallGraphBuilder {
|
||||
private final CodebaseContext context;
|
||||
private final ConstantResolver constantResolver;
|
||||
private String currentMethodFqn;
|
||||
private Map<String, List<CallEdge>> graph;
|
||||
|
||||
@lombok.Data
|
||||
private static class CallEdge {
|
||||
private final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
}
|
||||
|
||||
public CallGraphBuilder(CodebaseContext context) {
|
||||
this.context = context;
|
||||
this.constantResolver = new ConstantResolver();
|
||||
}
|
||||
|
||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||
List<CallChain> chains = new ArrayList<>();
|
||||
|
||||
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()
|
||||
.entryPoint(ep)
|
||||
.triggerPoint(resolvedTp)
|
||||
.methodChain(path)
|
||||
.contextMachineId(contextMachineId)
|
||||
.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;
|
||||
}
|
||||
|
||||
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
if (path.size() < 2) return tp;
|
||||
|
||||
String event = tp.getEvent();
|
||||
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
|
||||
|
||||
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--) {
|
||||
String target = path.get(i);
|
||||
String caller = path.get(i - 1);
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Find the edge from caller to target to get the argument passed
|
||||
List<CallEdge> edges = callGraph.get(caller);
|
||||
boolean found = false;
|
||||
if (edges != null) {
|
||||
for (CallEdge edge : edges) {
|
||||
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||
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 + methodSuffix;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) break; // Could not map argument
|
||||
}
|
||||
|
||||
// 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 (!methodSuffix.isEmpty() && methodSuffix.matches("\\.[a-zA-Z0-9_]+\\(\\)")
|
||||
&& currentParamName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
|
||||
String varName = currentParamName;
|
||||
String methodName = methodSuffix.substring(1, methodSuffix.length() - 2);
|
||||
|
||||
// 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) {
|
||||
List<String> typesToInspect = new ArrayList<>();
|
||||
typesToInspect.add(declaredType);
|
||||
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
return tp;
|
||||
}
|
||||
|
||||
private int getParameterIndex(String methodFqn, String paramName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||
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 (int i = 0; i < md.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(paramName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
}
|
||||
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);
|
||||
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);
|
||||
if (edges != null) {
|
||||
for (CallEdge edge : edges) {
|
||||
String target = edge.getTargetMethod();
|
||||
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
|
||||
// Persister signatures usually like: restore(stateMachine, contextObj)
|
||||
if (edge.getArguments().size() >= 2) {
|
||||
return edge.getArguments().get(1); // The contextObj / machineId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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(MethodInvocation node) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Object argObj : astArguments) {
|
||||
Expression expr = (Expression) argObj;
|
||||
|
||||
// Extract from lambda
|
||||
if (expr instanceof LambdaExpression le) {
|
||||
ASTNode body = le.getBody();
|
||||
if (body instanceof Expression bodyExpr) {
|
||||
expr = bodyExpr;
|
||||
} else if (body instanceof Block block) {
|
||||
for (Object stmtObj : block.statements()) {
|
||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||
expr = rs.getExpression();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, ...))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
String val;
|
||||
if (expr instanceof MethodInvocation mi && mi.getExpression() instanceof SimpleName
|
||||
&& mi.arguments().isEmpty()) {
|
||||
// Keep local getter chains (e.g. event.getType()) for downstream parameter tracing.
|
||||
val = expr.toString();
|
||||
} else {
|
||||
val = constantResolver.resolve(expr, context);
|
||||
if (val == null) {
|
||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||
}
|
||||
}
|
||||
args.add(val);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
String baseCalled = resolveCalledMethod(node);
|
||||
if (baseCalled == null) return Collections.emptyList();
|
||||
|
||||
List<String> allResolved = new ArrayList<>();
|
||||
allResolved.add(baseCalled);
|
||||
|
||||
if (baseCalled.contains(".")) {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||
|
||||
List<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
}
|
||||
}
|
||||
|
||||
return allResolved;
|
||||
}
|
||||
|
||||
private String resolveCalledMethod(MethodInvocation node) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return resolveMethodInType(td, methodName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||
if (binding != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
||||
return context.getFqn(declaringTd) + "." + methodName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
||||
|
||||
List<CallEdge> neighbors = graph.get(start);
|
||||
if (neighbors != null) {
|
||||
for (CallEdge edge : neighbors) {
|
||||
String neighbor = edge.getTargetMethod();
|
||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||
visited.remove(start);
|
||||
return new ArrayList<>(List.of(start, target));
|
||||
}
|
||||
List<String> path = findPath(neighbor, target, graph, visited);
|
||||
if (path != null) {
|
||||
path.add(0, start);
|
||||
visited.remove(start);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Path search dead-end at {} when looking for {}", start, target);
|
||||
}
|
||||
|
||||
visited.remove(start);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||
if (target.endsWith("." + neighbor)) return true;
|
||||
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
||||
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
||||
return simpleNeighbor.equals(simpleTarget);
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
private Expression traceVariable(Expression expr) {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||
if (enclosingMethod != null) {
|
||||
final Expression[] initializer = new Expression[1];
|
||||
enclosingMethod.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) {
|
||||
return traceVariable(initializer[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.GetterBodyConstantScanner;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
@@ -531,36 +532,7 @@ public class ConstantExtractor {
|
||||
if (className == null || className.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
||||
if (typeDeclaration != null) {
|
||||
direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
if (!typeDeclaration.isInterface() && !Modifier.isAbstract(typeDeclaration.getModifiers())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
List<String> implementations = context.getImplementations(className);
|
||||
if (implementations == null || implementations.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
|
||||
.representativeImplAccessor(className, methodName);
|
||||
if (precomputed.isPresent()) {
|
||||
return precomputed;
|
||||
}
|
||||
for (String implementation : implementations) {
|
||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||
if (implAccessor.isPresent()) {
|
||||
return implAccessor;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
return context.getAccessorIndex().lookup(className, methodName);
|
||||
}
|
||||
|
||||
private List<String> extractConstantGetterReturn(
|
||||
@@ -568,30 +540,8 @@ public class ConstantExtractor {
|
||||
String methodName,
|
||||
CompilationUnit contextCu,
|
||||
Set<String> visited) {
|
||||
TypeDeclaration typeDeclaration = contextCu != null
|
||||
? context.getTypeDeclaration(ownerFqn, contextCu)
|
||||
: null;
|
||||
if (typeDeclaration == null) {
|
||||
typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> constants = new ArrayList<>();
|
||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
extractConstantsFromExpression(node.getExpression(), constants);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return constants;
|
||||
return GetterBodyConstantScanner.extractConstantGetterReturn(
|
||||
context, this, constantResolver, ownerFqn, methodName, contextCu, visited);
|
||||
}
|
||||
|
||||
|
||||
@@ -699,10 +649,6 @@ public class ConstantExtractor {
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,18 +821,10 @@ public class ConstructorAnalyzer {
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
@@ -15,11 +14,18 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GenericEventDetector {
|
||||
private final CodebaseContext context;
|
||||
private final ConstantResolver constantResolver;
|
||||
private final List<LibraryHint> hints;
|
||||
private final TypeResolver typeResolver;
|
||||
|
||||
public GenericEventDetector(CodebaseContext context, ConstantResolver constantResolver, List<LibraryHint> hints) {
|
||||
this.context = context;
|
||||
this.constantResolver = constantResolver;
|
||||
this.hints = hints;
|
||||
this.typeResolver = new TypeResolver(context);
|
||||
}
|
||||
|
||||
/** Parameter names that route to a machine, not SM state enum constants. */
|
||||
private static final Set<String> ROUTING_PARAMETER_NAMES = Set.of(
|
||||
@@ -77,7 +83,7 @@ public class GenericEventDetector {
|
||||
eventValue = receiver != null ? receiver.toString() : null;
|
||||
}
|
||||
|
||||
MethodDeclaration method = findEnclosingMethod(node);
|
||||
MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
AbstractTypeDeclaration type = findEnclosingAbstractType(node);
|
||||
if (type == null) return;
|
||||
|
||||
@@ -149,8 +155,8 @@ public class GenericEventDetector {
|
||||
List<?> typeArgs = pt.typeArguments();
|
||||
if (typeArgs.size() >= 2) {
|
||||
CompilationUnit compilationUnit = (CompilationUnit) node.getRoot();
|
||||
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit);
|
||||
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit);
|
||||
String stateType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit);
|
||||
String eventType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit);
|
||||
return new String[]{stateType, eventType};
|
||||
}
|
||||
}
|
||||
@@ -162,7 +168,9 @@ public class GenericEventDetector {
|
||||
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||
if (builtTriggers != null) {
|
||||
for (TriggerPoint trigger : builtTriggers) {
|
||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||
}
|
||||
triggers.add(trigger);
|
||||
}
|
||||
}
|
||||
@@ -190,7 +198,9 @@ public class GenericEventDetector {
|
||||
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());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||
}
|
||||
triggers.add(trigger);
|
||||
}
|
||||
}
|
||||
@@ -245,7 +255,7 @@ public class GenericEventDetector {
|
||||
}
|
||||
}
|
||||
|
||||
MethodDeclaration method = findEnclosingMethod(node);
|
||||
MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
AbstractTypeDeclaration type = findEnclosingAbstractType(node);
|
||||
|
||||
if (type == null) return Collections.emptyList();
|
||||
@@ -453,7 +463,7 @@ public class GenericEventDetector {
|
||||
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||
MethodDeclaration enclosingMethod = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(expr);
|
||||
if (enclosingMethod != null) {
|
||||
// Find variable declaration
|
||||
final Expression[] initializer = new Expression[1];
|
||||
@@ -525,7 +535,7 @@ public class GenericEventDetector {
|
||||
return payloadExpr.toString();
|
||||
} 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 the provider's name so the call-graph engine can trace the lambda argument
|
||||
String traced = extractEventFromMessageBuilder(sn);
|
||||
return traced != null ? traced : sn.getIdentifier();
|
||||
}
|
||||
@@ -540,19 +550,6 @@ public class GenericEventDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
AbstractTypeDeclaration type = findEnclosingAbstractType(node);
|
||||
return type instanceof TypeDeclaration td ? td : null;
|
||||
}
|
||||
|
||||
private AbstractTypeDeclaration findEnclosingAbstractType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof AbstractTypeDeclaration)) {
|
||||
@@ -630,8 +627,8 @@ public class GenericEventDetector {
|
||||
List<?> typeArgs = pt.typeArguments();
|
||||
if (typeArgs.size() >= 2) {
|
||||
CompilationUnit cu = (CompilationUnit) node.getRoot();
|
||||
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu);
|
||||
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu);
|
||||
String stateType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(0), cu);
|
||||
String eventType = typeResolver.resolveTypeToFqn((Type) typeArgs.get(1), cu);
|
||||
|
||||
if (stateType != null && stateType.length() == 1) {
|
||||
stateType = resolveGenericTypeVariable(stateType, node, cu);
|
||||
@@ -661,7 +658,7 @@ public class GenericEventDetector {
|
||||
Type superclassType = td.getSuperclassType();
|
||||
if (superclassType instanceof ParameterizedType pt) {
|
||||
Type baseType = pt.getType();
|
||||
String baseName = resolveTypeToFqn(baseType, (CompilationUnit) implNode.getRoot());
|
||||
String baseName = typeResolver.resolveTypeToFqn(baseType, (CompilationUnit) implNode.getRoot());
|
||||
if (className.equals(baseName) || className.endsWith("." + baseName)) {
|
||||
int typeIndex = -1;
|
||||
List<?> typeParams = enclosingClass instanceof TypeDeclaration typeDecl
|
||||
@@ -678,7 +675,7 @@ public class GenericEventDetector {
|
||||
}
|
||||
if (typeIndex >= 0 && typeIndex < pt.typeArguments().size()) {
|
||||
Type concreteType = (Type) pt.typeArguments().get(typeIndex);
|
||||
return resolveTypeToFqn(concreteType, (CompilationUnit) implNode.getRoot());
|
||||
return typeResolver.resolveTypeToFqn(concreteType, (CompilationUnit) implNode.getRoot());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,7 +707,7 @@ public class GenericEventDetector {
|
||||
if (!(receiver instanceof SimpleName sn)) return null;
|
||||
String varName = sn.getIdentifier();
|
||||
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(node);
|
||||
MethodDeclaration enclosingMethod = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
if (enclosingMethod != null) {
|
||||
// Check params
|
||||
for (Object paramObj : enclosingMethod.parameters()) {
|
||||
@@ -752,41 +749,6 @@ public class GenericEventDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
|
||||
if (type == null) return null;
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
// Fallback to import matching
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
// Check package name
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
if (!packageName.isEmpty()) {
|
||||
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||
if (localTd != null) return context.getFqn(localTd);
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private static class AssignmentDag {
|
||||
final java.util.Map<String, List<Expression>> assignments = new java.util.HashMap<>();
|
||||
final java.util.Set<SingleVariableDeclaration> parameters = new java.util.HashSet<>();
|
||||
|
||||
@@ -1,157 +1,22 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
protected String currentMethodFqn;
|
||||
protected Map<String, List<CallEdge>> graph;
|
||||
|
||||
public HeuristicCallGraphEngine(CodebaseContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, List<CallEdge>> buildCallGraph() {
|
||||
Map<String, List<CallEdge>> cached = (Map<String, List<CallEdge>>) context.getCache().get("heuristicCallGraph");
|
||||
if (cached != null) {
|
||||
this.graph = cached;
|
||||
return cached;
|
||||
}
|
||||
graph = new HashMap<>();
|
||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
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());
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
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) {
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver);
|
||||
implEdge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
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());
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
context.getCache().put("heuristicCallGraph", graph);
|
||||
return graph;
|
||||
}
|
||||
|
||||
private String findDefiningClass(String className, String methodName) {
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) return null;
|
||||
|
||||
String resolvedMethodFqn = resolveMethodInTypeHierarchy(td, methodName);
|
||||
if (resolvedMethodFqn != null) {
|
||||
return resolvedMethodFqn.substring(0, resolvedMethodFqn.lastIndexOf('.'));
|
||||
}
|
||||
return null;
|
||||
@Override
|
||||
protected String callGraphCacheKey() {
|
||||
return "heuristicCallGraph";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveCalledMethod(MethodInvocation node) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
@@ -188,8 +53,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (fallbackTypeFqn != null) {
|
||||
return fallbackTypeFqn + "." + methodName;
|
||||
}
|
||||
String receiverName = sn.getIdentifier();
|
||||
return receiverName + "." + methodName;
|
||||
return sn.getIdentifier() + "." + methodName;
|
||||
}
|
||||
|
||||
if (receiver instanceof QualifiedName qn) {
|
||||
@@ -198,66 +62,9 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (td != null) {
|
||||
return fqn + "." + methodName;
|
||||
}
|
||||
return fqn + "." + methodName;
|
||||
return fqn + "." + methodName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||
Expression receiver = mi.getExpression();
|
||||
String receiverType = null;
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(mi);
|
||||
if (td != null) {
|
||||
receiverType = context.getFqn(td);
|
||||
}
|
||||
} else if (receiver instanceof SimpleName sn) {
|
||||
receiverType = resolveReceiverTypeFallback(sn);
|
||||
} else if (receiver instanceof FieldAccess fa) {
|
||||
receiverType = resolveReceiverTypeFallback(fa.getName());
|
||||
} else if (receiver instanceof MethodInvocation innerMi) {
|
||||
receiverType = resolveMethodInvocationReturnType(innerMi);
|
||||
}
|
||||
|
||||
if (receiverType == null) {
|
||||
ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
|
||||
if (binding != null) {
|
||||
receiverType = binding.getErasure().getQualifiedName();
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverType != null) {
|
||||
String rawType = receiverType;
|
||||
if (rawType.contains("<")) {
|
||||
rawType = rawType.substring(0, rawType.indexOf('<'));
|
||||
}
|
||||
if (rawType.equals("java.util.Map") || rawType.equals("Map")) {
|
||||
String valType = resolveMapValueType(mi);
|
||||
if (valType != null) return valType;
|
||||
}
|
||||
if (receiverType.contains("<")) {
|
||||
receiverType = receiverType.substring(0, receiverType.indexOf('<'));
|
||||
}
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
TypeDeclaration td = context.getTypeDeclaration(receiverType);
|
||||
if (td == null && receiverType.contains(".")) {
|
||||
String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
|
||||
td = context.getTypeDeclaration(simpleClass);
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getReturnType2() != null) {
|
||||
return resolveTypeToFqn(md.getReturnType2(), mi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
||||
if (methodBinding != null && methodBinding.getReturnType() != null) {
|
||||
return methodBinding.getReturnType().getErasure().getQualifiedName();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +1,42 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||
|
||||
@Slf4j
|
||||
public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
private final InjectionPointAnalyzer injectionAnalyzer;
|
||||
private String currentMethodFqn;
|
||||
private Map<String, List<CallEdge>> graph;
|
||||
|
||||
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
|
||||
super(context);
|
||||
this.injectionAnalyzer = injectionAnalyzer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, List<CallEdge>> buildCallGraph() {
|
||||
Map<String, List<CallEdge>> cached = (Map<String, List<CallEdge>>) context.getCache().get("jdtCallGraph");
|
||||
if (cached != null) {
|
||||
this.graph = cached;
|
||||
return cached;
|
||||
}
|
||||
graph = new HashMap<>();
|
||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
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());
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
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) {
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
List<String> implicitArgs = new ArrayList<>();
|
||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||
implicitArgs.add(node.getExpression().toString());
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver);
|
||||
implEdge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
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());
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
context.getCache().put("jdtCallGraph", graph);
|
||||
return graph;
|
||||
@Override
|
||||
protected String callGraphCacheKey() {
|
||||
return "jdtCallGraph";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
||||
protected String resolveArgument(Expression expr) {
|
||||
if (expr == null) {
|
||||
return "null";
|
||||
}
|
||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||
org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
||||
Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
||||
if (unwrappedBuilder != expr) {
|
||||
expr = unwrappedBuilder;
|
||||
}
|
||||
|
||||
// Trace variable to resolve local variable references
|
||||
org.eclipse.jdt.core.dom.Expression tracedExpr = traceVariable(expr);
|
||||
if (tracedExpr instanceof org.eclipse.jdt.core.dom.QualifiedName
|
||||
|| tracedExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation
|
||||
|| tracedExpr instanceof org.eclipse.jdt.core.dom.StringLiteral
|
||||
|| tracedExpr instanceof org.eclipse.jdt.core.dom.NumberLiteral) {
|
||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||
Expression tracedExpr = traceVariable(expr);
|
||||
if (tracedExpr instanceof QualifiedName
|
||||
|| tracedExpr instanceof ClassInstanceCreation
|
||||
|| tracedExpr instanceof StringLiteral
|
||||
|| tracedExpr instanceof NumberLiteral) {
|
||||
expr = tracedExpr;
|
||||
}
|
||||
|
||||
// Delegate to base class for lambda, method reference, and constructor unwrapping
|
||||
String result = super.resolveArgument(expr);
|
||||
if (result != null) {
|
||||
result = result.replaceAll("->\\s*yield\\s+", "-> ");
|
||||
@@ -173,6 +44,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveCalledMethod(MethodInvocation node) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
@@ -209,15 +81,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||
}
|
||||
return concreteFqn + "." + methodName;
|
||||
} else {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||
} else if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||
}
|
||||
} else if (log.isDebugEnabled()) {
|
||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +99,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
typeName = binding.getErasure().getName();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (Exception ignored) {
|
||||
// Ignore JDT internal exceptions
|
||||
}
|
||||
|
||||
@@ -246,7 +114,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
|
||||
if (typeName != null && !typeName.isEmpty()) {
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration resolvedTd = context.getTypeDeclaration(typeName);
|
||||
TypeDeclaration resolvedTd = context.getTypeDeclaration(typeName);
|
||||
if (resolvedTd != null && context.findMethodDeclaration(resolvedTd, methodName, true) != null) {
|
||||
return typeName + "." + methodName;
|
||||
}
|
||||
@@ -265,8 +133,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (fallbackTypeFqn != null) {
|
||||
return fallbackTypeFqn + "." + methodName;
|
||||
}
|
||||
String receiverName = sn.getIdentifier();
|
||||
return receiverName + "." + methodName;
|
||||
return sn.getIdentifier() + "." + methodName;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -290,55 +157,4 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||
Expression receiver = mi.getExpression();
|
||||
String receiverType = null;
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(mi);
|
||||
if (td != null) {
|
||||
receiverType = context.getFqn(td);
|
||||
}
|
||||
} else if (receiver instanceof SimpleName sn) {
|
||||
receiverType = resolveReceiverTypeFallback(sn);
|
||||
} else if (receiver instanceof FieldAccess fa) {
|
||||
receiverType = resolveReceiverTypeFallback(fa.getName());
|
||||
} else if (receiver instanceof MethodInvocation innerMi) {
|
||||
receiverType = resolveMethodInvocationReturnType(innerMi);
|
||||
}
|
||||
|
||||
if (receiverType == null) {
|
||||
ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
|
||||
if (binding != null) {
|
||||
receiverType = binding.getErasure().getQualifiedName();
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverType != null) {
|
||||
String rawType = receiverType;
|
||||
if (rawType.contains("<")) {
|
||||
rawType = rawType.substring(0, rawType.indexOf('<'));
|
||||
}
|
||||
if (rawType.equals("java.util.Map") || rawType.equals("Map")) {
|
||||
String valType = resolveMapValueType(mi);
|
||||
if (valType != null) return valType;
|
||||
}
|
||||
if (receiverType.contains("<")) {
|
||||
receiverType = receiverType.substring(0, receiverType.indexOf('<'));
|
||||
}
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
TypeDeclaration td = context.getTypeDeclaration(receiverType);
|
||||
if (td == null && receiverType.contains(".")) {
|
||||
String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
|
||||
td = context.getTypeDeclaration(simpleClass);
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getReturnType2() != null) {
|
||||
return resolveTypeToFqn(md.getReturnType2(), mi);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,19 @@ public final class JsonExportContextFactory {
|
||||
|
||||
public static Path findProjectRoot(Path start) {
|
||||
Path current = start != null ? start.toAbsolutePath().normalize() : null;
|
||||
Path settingsRoot = null;
|
||||
Path buildRoot = null;
|
||||
while (current != null) {
|
||||
if (Files.exists(current.resolve("settings.gradle"))
|
||||
|| Files.exists(current.resolve("pom.xml"))
|
||||
|| Files.exists(current.resolve("build.gradle"))) {
|
||||
return current;
|
||||
if (settingsRoot == null && Files.exists(current.resolve("settings.gradle"))) {
|
||||
settingsRoot = current;
|
||||
}
|
||||
if (buildRoot == null && (Files.exists(current.resolve("pom.xml"))
|
||||
|| Files.exists(current.resolve("build.gradle")))) {
|
||||
buildRoot = current;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
return settingsRoot != null ? settingsRoot : buildRoot;
|
||||
}
|
||||
|
||||
public static CodebaseContext scanProjectRoot(Path projectRoot, List<String> activeProfiles) throws IOException {
|
||||
|
||||
@@ -31,8 +31,8 @@ public class LifecycleDetector {
|
||||
}
|
||||
|
||||
private TriggerPoint buildLifecyclePoint(MethodInvocation node, CompilationUnit cu, String type) {
|
||||
MethodDeclaration method = findEnclosingMethod(node);
|
||||
TypeDeclaration typeDecl = findEnclosingType(node);
|
||||
MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
TypeDeclaration typeDecl = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||
|
||||
if (typeDecl == null) return null;
|
||||
|
||||
@@ -43,20 +43,4 @@ public class LifecycleDetector {
|
||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ 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;
|
||||
@@ -36,13 +33,13 @@ public class SpringComponentDetector {
|
||||
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if (typeName.endsWith("Scheduled")) {
|
||||
Map<String, String> meta = new HashMap<>();
|
||||
String cron = extractAnnotationValue(annotation, "cron");
|
||||
String cron = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "cron");
|
||||
if (!cron.isEmpty())
|
||||
meta.put("cron", cron);
|
||||
String fixedRate = extractAnnotationValue(annotation, "fixedRate");
|
||||
String fixedRate = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "fixedRate");
|
||||
if (!fixedRate.isEmpty())
|
||||
meta.put("fixedRate", fixedRate);
|
||||
String fixedDelay = extractAnnotationValue(annotation, "fixedDelay");
|
||||
String fixedDelay = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "fixedDelay");
|
||||
if (!fixedDelay.isEmpty())
|
||||
meta.put("fixedDelay", fixedDelay);
|
||||
|
||||
@@ -56,10 +53,10 @@ public class SpringComponentDetector {
|
||||
.build());
|
||||
} else if (typeName.endsWith("EventListener")) {
|
||||
Map<String, String> meta = new HashMap<>();
|
||||
String classes = extractAnnotationValue(annotation, "classes");
|
||||
String classes = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "classes");
|
||||
if (!classes.isEmpty())
|
||||
meta.put("classes", classes);
|
||||
String condition = extractAnnotationValue(annotation, "condition");
|
||||
String condition = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "condition");
|
||||
if (!condition.isEmpty())
|
||||
meta.put("condition", condition);
|
||||
|
||||
@@ -73,11 +70,11 @@ public class SpringComponentDetector {
|
||||
.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");
|
||||
String value = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "value");
|
||||
if (!value.isEmpty())
|
||||
meta.put("pointcut", value);
|
||||
else {
|
||||
String pointcut = extractAnnotationValue(annotation, "pointcut");
|
||||
String pointcut = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(annotation, "pointcut");
|
||||
if (!pointcut.isEmpty())
|
||||
meta.put("pointcut", pointcut);
|
||||
}
|
||||
@@ -98,20 +95,4 @@ public class SpringComponentDetector {
|
||||
});
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
import org.eclipse.jdt.core.dom.WildcardType;
|
||||
|
||||
public class TypeResolver {
|
||||
private final CodebaseContext context;
|
||||
|
||||
@@ -13,6 +15,73 @@ public class TypeResolver {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||
CompilationUnit cu = contextNode != null && contextNode.getRoot() instanceof CompilationUnit rootCu
|
||||
? rootCu
|
||||
: null;
|
||||
return resolveTypeToFqn(type, cu, Map.of());
|
||||
}
|
||||
|
||||
public String resolveTypeToFqn(Type type, CompilationUnit cu) {
|
||||
return resolveTypeToFqn(type, cu, Map.of());
|
||||
}
|
||||
|
||||
public String resolveTypeToFqn(Type type, CompilationUnit cu, Map<String, String> typeVarBindings) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
if (type instanceof WildcardType wildcardType) {
|
||||
Type bound = wildcardType.getBound();
|
||||
return bound != null ? resolveTypeToFqn(bound, cu, typeVarBindings) : null;
|
||||
}
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu, typeVarBindings);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
if (simpleName.contains("<")) {
|
||||
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
||||
}
|
||||
|
||||
if (typeVarBindings.containsKey(simpleName)) {
|
||||
return typeVarBindings.get(simpleName);
|
||||
}
|
||||
|
||||
if (cu != null) {
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
if (!packageName.isEmpty()) {
|
||||
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||
if (localTd != null) {
|
||||
return context.getFqn(localTd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
public int getParameterIndex(String methodFqn, String paramName) {
|
||||
return getParameterIndex(methodFqn, paramName, false);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorNaming;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
@@ -18,11 +20,13 @@ public class VariableTracer {
|
||||
|
||||
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
||||
private final Map<String, String> variableDeclaredTypeCache = new HashMap<>();
|
||||
private final FieldInitializerFinder fieldInitializerFinder;
|
||||
|
||||
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||
this.context = context;
|
||||
this.constantResolver = constantResolver;
|
||||
this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
|
||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
||||
}
|
||||
|
||||
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
||||
@@ -74,9 +78,9 @@ public class VariableTracer {
|
||||
|
||||
String setterMethodName;
|
||||
String indexedFieldName;
|
||||
if (isBeanStyleAccessorName(getterName)) {
|
||||
String propName = propertyNameFromAccessor(getterName);
|
||||
setterMethodName = "set" + capitalizeProperty(propName);
|
||||
if (AccessorNaming.isBeanStyleAccessorName(getterName)) {
|
||||
String propName = AccessorNaming.propertyNameFromAccessor(getterName);
|
||||
setterMethodName = "set" + AccessorNaming.capitalizeProperty(propName);
|
||||
indexedFieldName = propName;
|
||||
|
||||
String varTypeName = getVariableDeclaredType(methodFqn, varName);
|
||||
@@ -110,32 +114,6 @@ public class VariableTracer {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isBeanStyleAccessorName(String accessorName) {
|
||||
return (accessorName.startsWith("get") && accessorName.length() > 3)
|
||||
|| (accessorName.startsWith("is") && accessorName.length() > 2)
|
||||
|| (accessorName.startsWith("set") && accessorName.length() > 3);
|
||||
}
|
||||
|
||||
private static String propertyNameFromAccessor(String getterName) {
|
||||
if (getterName.startsWith("get") && getterName.length() > 3) {
|
||||
return TrivialAccessorDetector.decapitalize(getterName.substring(3));
|
||||
}
|
||||
if (getterName.startsWith("is") && getterName.length() > 2) {
|
||||
return TrivialAccessorDetector.decapitalize(getterName.substring(2));
|
||||
}
|
||||
return getterName;
|
||||
}
|
||||
|
||||
private static String capitalizeProperty(String propertyName) {
|
||||
if (propertyName == null || propertyName.isEmpty()) {
|
||||
return propertyName;
|
||||
}
|
||||
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
|
||||
return propertyName;
|
||||
}
|
||||
return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
|
||||
}
|
||||
|
||||
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
|
||||
if (td == null) {
|
||||
return null;
|
||||
@@ -150,55 +128,15 @@ public class VariableTracer {
|
||||
return indexed.get();
|
||||
}
|
||||
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
||||
return fd.getType().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
String result = getFieldType(superTd, fieldName, context, visited);
|
||||
if (result != null) return result;
|
||||
}
|
||||
}
|
||||
|
||||
for (Object intfObj : td.superInterfaceTypes()) {
|
||||
Type intfType = (Type) intfObj;
|
||||
String intfName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(intfType);
|
||||
String resolvedIntf = resolveTypeFqn(intfName, context, td);
|
||||
if (resolvedIntf != null) {
|
||||
TypeDeclaration intfTd = context.getTypeDeclaration(resolvedIntf);
|
||||
if (intfTd != null) {
|
||||
String result = getFieldType(intfTd, fieldName, context, visited);
|
||||
if (result != null) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return fieldInitializerFinder.resolveFieldTypeName(td, fieldName, visited);
|
||||
}
|
||||
|
||||
private String resolveTypeFqn(String simpleName, CodebaseContext context, TypeDeclaration td) {
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
if (cu != null) {
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String name = imp.getName().getFullyQualifiedName();
|
||||
if (name.endsWith("." + simpleName)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
if (cu.getPackage() != null) {
|
||||
return cu.getPackage().getName().getFullyQualifiedName() + "." + simpleName;
|
||||
}
|
||||
}
|
||||
return simpleName;
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||
}
|
||||
|
||||
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
||||
@@ -693,22 +631,6 @@ public class VariableTracer {
|
||||
return true;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
|
||||
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||
return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> resolveTargetMethodFqn(miArg, methodFqn));
|
||||
}
|
||||
|
||||
@@ -31,7 +31,17 @@ public class SpringDependencyResolver {
|
||||
.filter(bean -> requiredTypeFqn.equals(bean.getTypeFqn()) || bean.getAssignableTypes().contains(requiredTypeFqn))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.debug("CANDIDATES AFTER TYPE: {}", candidates.stream().map(c -> c.getTypeFqn() + " (primary=" + c.isPrimary() + " names=" + c.getBeanNames() + " qual=" + c.getQualifiers() + ")").collect(Collectors.toList()));
|
||||
log.debug("CANDIDATES AFTER TYPE: {}", candidates.size());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"CANDIDATE DETAILS: {}",
|
||||
candidates.stream()
|
||||
.map(c -> c.getTypeFqn()
|
||||
+ " (primary=" + c.isPrimary()
|
||||
+ " names=" + c.getBeanNames()
|
||||
+ " qual=" + c.getQualifiers() + ")")
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
if (candidates.isEmpty() || candidates.size() == 1) {
|
||||
log.debug("RETURNING: {}", candidates.size());
|
||||
return candidates;
|
||||
|
||||
@@ -7,6 +7,7 @@ import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.ArrayList;
|
||||
@@ -349,7 +350,7 @@ public class AstTransitionParser {
|
||||
}
|
||||
} else {
|
||||
// FALLBACK: Manual resolution by name if binding failed
|
||||
TypeDeclaration currentClass = findEnclosingType(mi);
|
||||
TypeDeclaration currentClass = AstUtils.findEnclosingType(mi);
|
||||
if (currentClass != null) {
|
||||
return resolveMethodManually(mi.getName().getIdentifier(), currentClass, context);
|
||||
}
|
||||
@@ -393,7 +394,7 @@ public class AstTransitionParser {
|
||||
typeFqn = binding.getQualifiedName();
|
||||
} else if (expr instanceof SimpleName sn) {
|
||||
// Check enclosing method first for local variables
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||
MethodDeclaration enclosingMethod = AstUtils.findEnclosingMethod(expr);
|
||||
if (enclosingMethod != null) {
|
||||
final String[] foundType = {null};
|
||||
final Expression[] foundInitializer = {null};
|
||||
@@ -424,7 +425,7 @@ public class AstTransitionParser {
|
||||
}
|
||||
|
||||
if (typeFqn == null) {
|
||||
TypeDeclaration enclosingClass = findEnclosingType(expr);
|
||||
TypeDeclaration enclosingClass = AstUtils.findEnclosingType(expr);
|
||||
if (enclosingClass != null) {
|
||||
for (FieldDeclaration fd : enclosingClass.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
@@ -480,15 +481,6 @@ public class AstTransitionParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode current = node;
|
||||
while (current != null) {
|
||||
if (current instanceof MethodDeclaration md) return md;
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String resolveMethodManually(String methodName, TypeDeclaration td, CodebaseContext context) {
|
||||
// Search in this class
|
||||
for (MethodDeclaration md : td.getMethods()) {
|
||||
@@ -520,21 +512,12 @@ public class AstTransitionParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode current = node;
|
||||
while (current != null) {
|
||||
if (current instanceof TypeDeclaration td) return td;
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void parseGuard(Object arg, Transition t, CompilationUnit cu, CodebaseContext context) {
|
||||
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
||||
if (quotedExpr != null) {
|
||||
String internalLogic = extractInternalLogic(quotedExpr.getExpression(), cu, context);
|
||||
int lineNumber = cu.getLineNumber(quotedExpr.getExpression().getStartPosition());
|
||||
TypeDeclaration td = findEnclosingType(quotedExpr.getExpression());
|
||||
TypeDeclaration td = AstUtils.findEnclosingType(quotedExpr.getExpression());
|
||||
String fqn = td != null ? context.getFqn(td) : null;
|
||||
String sourceFile = fqn != null ? context.getRelativePath(fqn) : null;
|
||||
t.getGuards().add(Guard.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber, fqn, sourceFile));
|
||||
@@ -546,7 +529,7 @@ public class AstTransitionParser {
|
||||
if (quotedExpr != null) {
|
||||
String internalLogic = extractInternalLogic(quotedExpr.getExpression(), cu, context);
|
||||
int lineNumber = cu.getLineNumber(quotedExpr.getExpression().getStartPosition());
|
||||
TypeDeclaration td = findEnclosingType(quotedExpr.getExpression());
|
||||
TypeDeclaration td = AstUtils.findEnclosingType(quotedExpr.getExpression());
|
||||
String fqn = td != null ? context.getFqn(td) : null;
|
||||
String sourceFile = fqn != null ? context.getRelativePath(fqn) : null;
|
||||
t.getActions().add(Action.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber, fqn, sourceFile));
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.Annotation;
|
||||
import org.eclipse.jdt.core.dom.ArrayType;
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
import org.eclipse.jdt.core.dom.MemberValuePair;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.Name;
|
||||
import org.eclipse.jdt.core.dom.NormalAnnotation;
|
||||
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
|
||||
import org.eclipse.jdt.core.dom.NameQualifiedType;
|
||||
import org.eclipse.jdt.core.dom.ParameterizedType;
|
||||
import org.eclipse.jdt.core.dom.PrimitiveType;
|
||||
@@ -9,11 +15,59 @@ import org.eclipse.jdt.core.dom.QualifiedName;
|
||||
import org.eclipse.jdt.core.dom.QualifiedType;
|
||||
import org.eclipse.jdt.core.dom.SimpleType;
|
||||
import org.eclipse.jdt.core.dom.Type;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
public final class AstUtils {
|
||||
private AstUtils() {
|
||||
}
|
||||
|
||||
public static MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
|
||||
public static TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
|
||||
public static String simpleName(String fqn) {
|
||||
if (fqn == null || fqn.isEmpty()) {
|
||||
return fqn;
|
||||
}
|
||||
int dot = fqn.lastIndexOf('.');
|
||||
return dot >= 0 ? fqn.substring(dot + 1) : fqn;
|
||||
}
|
||||
|
||||
public static String qualifier(String fqn) {
|
||||
if (fqn == null || !fqn.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
public static String extractAnnotationMember(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 "";
|
||||
}
|
||||
|
||||
public static String extractSimpleTypeName(Type type) {
|
||||
if (type.isSimpleType()) {
|
||||
Name name = ((SimpleType) type).getName();
|
||||
|
||||
@@ -14,7 +14,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
||||
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
||||
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
|
||||
private final Map<String, Set<String>> compatibleContextTypesCache = new HashMap<>();
|
||||
|
||||
private static final ThreadLocal<List<String>> CURRENT_PATH = new ThreadLocal<>();
|
||||
|
||||
@@ -35,18 +34,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
}
|
||||
|
||||
public void clearAnalysisCaches() {
|
||||
compatibleContextTypesCache.clear();
|
||||
context.getClassCompatibilityCache().clearMemoCache();
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode current = node;
|
||||
while (current != null) {
|
||||
if (current instanceof MethodDeclaration) {
|
||||
return (MethodDeclaration) current;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
return AstUtils.findEnclosingMethod(node);
|
||||
}
|
||||
|
||||
private ReachingDefinitions getReachingDefinitionsForMethod(MethodDeclaration method) {
|
||||
@@ -1785,8 +1777,8 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
}
|
||||
|
||||
private Set<String> getCompatibleContextTypes(List<String> currentPath, String declaringClassFqn) {
|
||||
String cacheKey = String.valueOf(currentPath) + "#" + declaringClassFqn;
|
||||
return compatibleContextTypesCache.computeIfAbsent(
|
||||
String cacheKey = "contextTypes:" + currentPath + "#" + declaringClassFqn;
|
||||
return context.getClassCompatibilityCache().memoize(
|
||||
cacheKey, ignored -> computeCompatibleContextTypes(currentPath, declaringClassFqn));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
import java.util.List;
|
||||
|
||||
public class LegacyDataFlowModel implements DataFlowModel {
|
||||
private final VariableTracer variableTracer;
|
||||
|
||||
public LegacyDataFlowModel(VariableTracer variableTracer) {
|
||||
this.variableTracer = variableTracer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Expression> getReachingDefinitions(Expression expr) {
|
||||
if (variableTracer == null || expr == null) {
|
||||
return expr != null ? List.of(expr) : List.of();
|
||||
}
|
||||
return variableTracer.traceVariableAll(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||
if (expr == null) return null;
|
||||
if (variableTracer == null) {
|
||||
return context.resolveExpression(expr);
|
||||
}
|
||||
Expression traced = variableTracer.traceVariable(expr);
|
||||
return context.resolveExpression(traced);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class StateResolver {
|
||||
// 2. Check for @Value fields (SimpleName)
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String fieldName = sn.getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(sn);
|
||||
TypeDeclaration td = click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(sn);
|
||||
if (td != null) {
|
||||
String value = findValueFromField(td, fieldName);
|
||||
if (value != null) {
|
||||
@@ -69,21 +69,11 @@ public class StateResolver {
|
||||
}
|
||||
|
||||
private String extractAnnotationValue(Annotation ann) {
|
||||
String raw;
|
||||
if (ann instanceof SingleMemberAnnotation sma) {
|
||||
raw = stripQuotes(sma.getValue().toString());
|
||||
} else if (ann instanceof NormalAnnotation na) {
|
||||
raw = null;
|
||||
for (Object pairObj : na.values()) {
|
||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||
if ("value".equals(pair.getName().getIdentifier())) {
|
||||
raw = stripQuotes(pair.getValue().toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String raw = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractAnnotationMember(ann, "value");
|
||||
if (raw.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
raw = stripQuotes(raw);
|
||||
if (raw != null && raw.contains("${")) {
|
||||
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
|
||||
raw, java.util.Collections.emptyMap());
|
||||
@@ -96,14 +86,6 @@ public class StateResolver {
|
||||
return s.replaceAll("^\"|\"$", "");
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
|
||||
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
|
||||
String qualifier = qn.getQualifier().toString();
|
||||
String name = qn.getName().getIdentifier();
|
||||
|
||||
@@ -177,20 +177,24 @@ public class ExportService {
|
||||
}
|
||||
if (sourceRoot != null) {
|
||||
log.info("JSON re-export: scanning source project at {}", sourceRoot);
|
||||
context = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
||||
.scanProjectRoot(sourceRoot, activeProfiles);
|
||||
try {
|
||||
context = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
||||
.scanProjectRoot(sourceRoot, activeProfiles);
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("JSON re-export: source scan failed; using embedded machine types if present", ex);
|
||||
}
|
||||
} else {
|
||||
log.info("JSON re-export: no source project found; using embedded machine types if present");
|
||||
}
|
||||
|
||||
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory.resolveMachineTypes(
|
||||
result.getName(),
|
||||
result.getStateTypeFqn(),
|
||||
result.getEventTypeFqn(),
|
||||
context);
|
||||
AnalysisResultFinalizer.applyCanonicalization(result, machineTypes);
|
||||
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
||||
AnalysisResultFinalizer.finalizeResult(result, context, machineTypes);
|
||||
|
||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class EnricherPipelineRegressionTest {
|
||||
|
||||
@Test
|
||||
void shouldCanonicalizeChainTriggerEventsBeforeLinker(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleMachine(tempDir);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TriggerPoint rawTrigger = TriggerPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.event("OrderEvent.PAY")
|
||||
.sourceState("OrderState.NEW")
|
||||
.eventTypeFqn("OrderEvent")
|
||||
.stateTypeFqn("OrderState")
|
||||
.build();
|
||||
|
||||
CallChain rawChain = CallChain.builder()
|
||||
.triggerPoint(rawTrigger)
|
||||
.build();
|
||||
|
||||
CodebaseIntelligenceProvider intelligence = new CodebaseIntelligenceProvider() {
|
||||
@Override
|
||||
public List<TriggerPoint> findTriggerPoints() {
|
||||
return List.of(rawTrigger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EntryPoint> findEntryPoints() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
return List.of(rawChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, String>> resolveProperties() {
|
||||
return Map.of();
|
||||
}
|
||||
};
|
||||
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));
|
||||
transition.setSourceStates(List.of(State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));
|
||||
transition.setTargetStates(List.of(State.of("OrderState.PAID", "com.example.order.OrderState.PAID")));
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.transitions(List.of(transition))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.entryPoints(List.of())
|
||||
.triggers(List.of(rawTrigger))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
new CallChainEnricher().enrich(result, context, intelligence);
|
||||
new TriggerCanonicalizationEnricher().enrich(result, context, intelligence);
|
||||
new TransitionLinkerEnricher().enrich(result, context, intelligence);
|
||||
|
||||
TriggerPoint chainTrigger = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(chainTrigger.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
assertThat(chainTrigger.getSourceState()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(result.getMetadata().getTriggers().get(0).getEvent())
|
||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
private static void writeSampleMachine(Path tempDir) throws IOException {
|
||||
Path orderPkg = tempDir.resolve("com/example/order");
|
||||
Path configPkg = tempDir.resolve("com/example/config");
|
||||
Path webPkg = tempDir.resolve("com/example/web");
|
||||
Files.createDirectories(orderPkg);
|
||||
Files.createDirectories(configPkg);
|
||||
Files.createDirectories(webPkg);
|
||||
|
||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
||||
"""
|
||||
package com.example.order;
|
||||
public enum OrderState { NEW, PAID }
|
||||
""");
|
||||
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
||||
"""
|
||||
package com.example.order;
|
||||
public enum OrderEvent { PAY }
|
||||
""");
|
||||
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
||||
"""
|
||||
package com.example.config;
|
||||
import com.example.order.OrderEvent;
|
||||
import com.example.order.OrderState;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
public class OrderStateMachineConfiguration
|
||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||
}
|
||||
""");
|
||||
Files.writeString(webPkg.resolve("OrderController.java"),
|
||||
"""
|
||||
package com.example.web;
|
||||
public class OrderController {
|
||||
public void pay() {}
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,46 @@ class MachineEnumCanonicalizerTest {
|
||||
.isEqualTo("getType()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSyncFnFormRawNameWhenFullIdentifierAlreadyCanonical(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleConfig(tempDir);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
||||
"com.example.config.OrderStateMachineConfiguration", context);
|
||||
|
||||
State mismatched = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
|
||||
State canonical = MachineEnumCanonicalizer.canonicalizeStates(Set.of(mismatched), types.stateTypeFqn())
|
||||
.iterator().next();
|
||||
|
||||
assertThat(canonical.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(canonical.rawName()).isEqualTo("OrderState.NEW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDedupeStatesByCanonicalFullIdentifier(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleConfig(tempDir);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
||||
"com.example.config.OrderStateMachineConfiguration", context);
|
||||
|
||||
State shortForm = State.of("OrderState.NEW", "OrderState.NEW");
|
||||
State packageForm = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
|
||||
|
||||
Set<State> deduped = MachineEnumCanonicalizer.canonicalizeStates(
|
||||
Set.of(shortForm, packageForm), types.stateTypeFqn());
|
||||
|
||||
assertThat(deduped).hasSize(1);
|
||||
State only = deduped.iterator().next();
|
||||
assertThat(only.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(only.rawName()).isEqualTo("OrderState.NEW");
|
||||
}
|
||||
|
||||
private static void writeSampleConfig(Path tempDir) throws IOException {
|
||||
Path orderPkg = tempDir.resolve("com/example/order");
|
||||
Path configPkg = tempDir.resolve("com/example/config");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,374 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||
import click.kamil.springstatemachineexporter.service.JsonImportService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression gate: after the full {@link ExportService} pipeline, enum-backed machines must not
|
||||
* contain mixed short-form ({@code OrderState.NEW}) and package-canonical
|
||||
* ({@code com.example.order.OrderState.NEW}) identifiers in the same export.
|
||||
*/
|
||||
class ExportPipelineIdentifierConsistencyTest {
|
||||
|
||||
@Test
|
||||
void jsonReExportShouldNotMixShortAndPackageCanonicalStateIdentifiers(@TempDir Path tempDir) throws Exception {
|
||||
writeSampleProject(tempDir);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.sourceState("OrderState.NEW")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.sourceFile("OrderController.java")
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
||||
.build();
|
||||
CallChain chain = CallChain.builder().triggerPoint(trigger).build();
|
||||
|
||||
AnalysisResult shortForm = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.states(Set.of(
|
||||
State.of("OrderState.NEW", "OrderState.NEW"),
|
||||
State.of("UNDEFINED", "com.example.order.OrderState.PAID")))
|
||||
.transitions(List.of(
|
||||
shortTransition("OrderEvent.PAY", "OrderState.NEW", "OrderState.PAID"),
|
||||
multiSourceTransition("OrderEvent.PAY", List.of("OrderState.NEW", "OrderState.PAID"), "OrderState.PAID")))
|
||||
.startStates(Set.of("OrderState.NEW"))
|
||||
.endStates(Set.of("OrderState.PAID"))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.triggers(List.of(trigger))
|
||||
.callChains(List.of(chain))
|
||||
.properties(Map.of("default", Map.of(
|
||||
"app.event", "OrderEvent.PAY",
|
||||
"app.state", "OrderState.NEW")))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Path jsonFile = tempDir.resolve("machine.json");
|
||||
Files.writeString(jsonFile, new JsonExporter().export(shortForm, ExportOptions.builder().build()));
|
||||
|
||||
Path outputDir = tempDir.resolve("out");
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runJsonExporter(jsonFile, outputDir, List.of("json"), List.of("default"));
|
||||
|
||||
AnalysisResult exported = new JsonImportService().importAnalysisResult(
|
||||
outputDir.resolve("com.example.config.OrderStateMachineConfiguration")
|
||||
.resolve("com.example.config.OrderStateMachineConfiguration.json"));
|
||||
|
||||
assertNoMixedEnumIdentifiers(exported);
|
||||
assertThat(exported.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void astExportShouldNotMixShortAndPackageCanonicalStateIdentifiers(@TempDir Path tempDir) throws Exception {
|
||||
Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise");
|
||||
Path outputDir = tempDir.resolve("enterprise-out");
|
||||
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runExporter(enterprise, outputDir, List.of("json"), true, List.of());
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
try (var dirs = Files.list(outputDir)) {
|
||||
List<Path> machineDirs = dirs.filter(Files::isDirectory).toList();
|
||||
assertThat(machineDirs).isNotEmpty();
|
||||
for (Path machineDir : machineDirs) {
|
||||
Path json = machineDir.resolve(machineDir.getFileName() + ".json");
|
||||
JsonNode root = mapper.readTree(json.toFile());
|
||||
if (!root.hasNonNull("stateTypeFqn") || !root.get("stateTypeFqn").asText().contains(".")) {
|
||||
continue;
|
||||
}
|
||||
AnalysisResult result = new JsonImportService().importAnalysisResult(json);
|
||||
assertNoMixedEnumIdentifiers(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertyResolutionShouldCanonicalizeCallChainTriggersBeforeRelink(@TempDir Path tempDir) throws Exception {
|
||||
writeSampleProject(tempDir);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("${app.event}")
|
||||
.sourceState("${app.state}")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.sourceFile("OrderController.java")
|
||||
.polymorphicEvents(List.of("${app.event}"))
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.build();
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(trigger)
|
||||
.matchedTransitions(List.of(MatchedTransition.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.sourceState("OrderState.NEW")
|
||||
.targetState("OrderState.PAID")
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(shortTransition("OrderEvent.PAY", "OrderState.NEW", "OrderState.PAID")))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.triggers(List.of(trigger))
|
||||
.callChains(List.of(chain))
|
||||
.properties(Map.of("default", Map.of(
|
||||
"app.event", "OrderEvent.PAY",
|
||||
"app.state", "OrderState.NEW")))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
result.applyResolution(Map.of("app.event", "OrderEvent.PAY", "app.state", "OrderState.NEW"));
|
||||
|
||||
TriggerPoint chainTrigger = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(chainTrigger.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
assertThat(chainTrigger.getSourceState()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getSourceState())
|
||||
.isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertNoMixedEnumIdentifiers(result);
|
||||
}
|
||||
|
||||
static void assertNoMixedEnumIdentifiers(AnalysisResult result) {
|
||||
String stateTypeFqn = result.getStateTypeFqn();
|
||||
String eventTypeFqn = result.getEventTypeFqn();
|
||||
if (stateTypeFqn == null || !stateTypeFqn.contains(".")
|
||||
|| MachineEnumCanonicalizer.isStringOrPrimitiveType(stateTypeFqn)) {
|
||||
return;
|
||||
}
|
||||
if (eventTypeFqn == null || !eventTypeFqn.contains(".")
|
||||
|| MachineEnumCanonicalizer.isStringOrPrimitiveType(eventTypeFqn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> stateIds = collectStateIdentifiers(result);
|
||||
Set<String> eventIds = collectEventIdentifiers(result);
|
||||
|
||||
assertCanonicalSet(stateIds, stateTypeFqn, "state");
|
||||
assertCanonicalSet(eventIds, eventTypeFqn, "event");
|
||||
assertNoShortFormWhenPackageFormPresent(stateIds, stateTypeFqn);
|
||||
assertNoShortFormWhenPackageFormPresent(eventIds, eventTypeFqn);
|
||||
}
|
||||
|
||||
private static void assertCanonicalSet(Set<String> identifiers, String enumTypeFqn, String label) {
|
||||
for (String id : identifiers) {
|
||||
if (id == null || id.isBlank() || id.startsWith("<") || id.startsWith("\"")) {
|
||||
continue;
|
||||
}
|
||||
if (!MachineEnumCanonicalizer.isMachineEnumReference(id, enumTypeFqn)) {
|
||||
continue;
|
||||
}
|
||||
assertThat(id)
|
||||
.as("%s identifier must be package-canonical: %s", label, id)
|
||||
.isEqualTo(MachineEnumCanonicalizer.canonicalizeLabel(id, enumTypeFqn));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNoShortFormWhenPackageFormPresent(Set<String> identifiers, String enumTypeFqn) {
|
||||
Set<String> packageForms = new HashSet<>();
|
||||
Set<String> shortForms = new HashSet<>();
|
||||
for (String id : identifiers) {
|
||||
if (id == null || !MachineEnumCanonicalizer.isMachineEnumReference(id, enumTypeFqn)) {
|
||||
continue;
|
||||
}
|
||||
if (id.startsWith(enumTypeFqn + ".")) {
|
||||
packageForms.add(id);
|
||||
String fn = fnForm(id);
|
||||
if (fn != null) {
|
||||
shortForms.add(fn);
|
||||
}
|
||||
} else {
|
||||
shortForms.add(id);
|
||||
}
|
||||
}
|
||||
for (String pkg : packageForms) {
|
||||
String fn = fnForm(pkg);
|
||||
assertThat(identifiers)
|
||||
.as("mixed short/package identifiers for %s", pkg)
|
||||
.doesNotContain(fn);
|
||||
}
|
||||
assertThat(shortForms.stream().filter(id -> !id.startsWith(enumTypeFqn + ".")).toList())
|
||||
.as("unexpected bare short forms while package forms exist: %s vs %s", shortForms, packageForms)
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
private static String fnForm(String identifier) {
|
||||
if (identifier == null || identifier.isBlank()) {
|
||||
return identifier;
|
||||
}
|
||||
int lastDot = identifier.lastIndexOf('.');
|
||||
if (lastDot <= 0) {
|
||||
return identifier;
|
||||
}
|
||||
int prevDot = identifier.lastIndexOf('.', lastDot - 1);
|
||||
return prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
|
||||
}
|
||||
|
||||
private static Set<String> collectStateIdentifiers(AnalysisResult result) {
|
||||
Set<String> ids = new LinkedHashSet<>();
|
||||
if (result.getStates() != null) {
|
||||
result.getStates().forEach(s -> addIfPresent(ids, s.fullIdentifier()));
|
||||
}
|
||||
if (result.getStartStates() != null) {
|
||||
ids.addAll(result.getStartStates());
|
||||
}
|
||||
if (result.getEndStates() != null) {
|
||||
ids.addAll(result.getEndStates());
|
||||
}
|
||||
if (result.getTransitions() != null) {
|
||||
for (Transition t : result.getTransitions()) {
|
||||
addStates(ids, t.getSourceStates());
|
||||
addStates(ids, t.getTargetStates());
|
||||
}
|
||||
}
|
||||
if (result.getMetadata() != null) {
|
||||
if (result.getMetadata().getTriggers() != null) {
|
||||
for (TriggerPoint trigger : result.getMetadata().getTriggers()) {
|
||||
addIfPresent(ids, trigger.getSourceState());
|
||||
}
|
||||
}
|
||||
if (result.getMetadata().getCallChains() != null) {
|
||||
for (CallChain chain : result.getMetadata().getCallChains()) {
|
||||
if (chain.getTriggerPoint() != null) {
|
||||
addIfPresent(ids, chain.getTriggerPoint().getSourceState());
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null) {
|
||||
for (MatchedTransition mt : chain.getMatchedTransitions()) {
|
||||
addIfPresent(ids, mt.getSourceState());
|
||||
addIfPresent(ids, mt.getTargetState());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ids.removeIf(id -> id == null || id.isBlank());
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static Set<String> collectEventIdentifiers(AnalysisResult result) {
|
||||
Set<String> ids = new LinkedHashSet<>();
|
||||
if (result.getTransitions() != null) {
|
||||
for (Transition t : result.getTransitions()) {
|
||||
if (t.getEvent() != null) {
|
||||
addIfPresent(ids, t.getEvent().fullIdentifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.getMetadata() != null) {
|
||||
if (result.getMetadata().getTriggers() != null) {
|
||||
for (TriggerPoint trigger : result.getMetadata().getTriggers()) {
|
||||
addIfPresent(ids, trigger.getEvent());
|
||||
if (trigger.getPolymorphicEvents() != null) {
|
||||
ids.addAll(trigger.getPolymorphicEvents());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.getMetadata().getCallChains() != null) {
|
||||
for (CallChain chain : result.getMetadata().getCallChains()) {
|
||||
if (chain.getTriggerPoint() != null) {
|
||||
addIfPresent(ids, chain.getTriggerPoint().getEvent());
|
||||
if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
|
||||
ids.addAll(chain.getTriggerPoint().getPolymorphicEvents());
|
||||
}
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null) {
|
||||
for (MatchedTransition mt : chain.getMatchedTransitions()) {
|
||||
addIfPresent(ids, mt.getEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ids.removeIf(id -> id == null || id.isBlank());
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static void addStates(Set<String> ids, List<State> states) {
|
||||
if (states == null) {
|
||||
return;
|
||||
}
|
||||
for (State state : states) {
|
||||
addIfPresent(ids, state.fullIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
private static void addIfPresent(Set<String> ids, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
ids.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static Transition shortTransition(String event, String source, String target) {
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of(event, event));
|
||||
transition.setSourceStates(List.of(State.of(source, source)));
|
||||
transition.setTargetStates(List.of(State.of(target, target)));
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static Transition multiSourceTransition(String event, List<String> sources, String target) {
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of(event, event));
|
||||
transition.setSourceStates(sources.stream().map(s -> State.of(s, s)).toList());
|
||||
transition.setTargetStates(List.of(State.of(target, target)));
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static void writeSampleProject(Path projectRoot) throws Exception {
|
||||
Path javaRoot = projectRoot.resolve("src/main/java");
|
||||
Path orderPkg = javaRoot.resolve("com/example/order");
|
||||
Path configPkg = javaRoot.resolve("com/example/config");
|
||||
Files.createDirectories(orderPkg);
|
||||
Files.createDirectories(configPkg);
|
||||
Files.writeString(projectRoot.resolve("build.gradle"), "plugins { id 'java' }");
|
||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
||||
"package com.example.order; public enum OrderState { NEW, PAID }");
|
||||
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
||||
"package com.example.order; public enum OrderEvent { PAY }");
|
||||
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
||||
"""
|
||||
package com.example.config;
|
||||
import com.example.order.OrderEvent;
|
||||
import com.example.order.OrderState;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
public class OrderStateMachineConfiguration
|
||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
Path current = Path.of(".").toAbsolutePath();
|
||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||
@@ -16,6 +17,7 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -95,6 +97,75 @@ class JsonRoundTripCanonicalizationTest {
|
||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRelinkMatchedTransitionsAfterPreCanonicalizingShortFormJson(@TempDir Path tempDir) throws Exception {
|
||||
writeSampleProject(tempDir);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.sourceState("OrderState.NEW")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.sourceFile("OrderController.java")
|
||||
.build();
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(trigger)
|
||||
.build();
|
||||
|
||||
AnalysisResult shortForm = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(shortTransition(
|
||||
"OrderEvent.PAY",
|
||||
"OrderState.NEW",
|
||||
"OrderState.PAID")))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.triggers(List.of(trigger))
|
||||
.callChains(List.of(chain))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Path jsonFile = tempDir.resolve("machine.json");
|
||||
Files.writeString(jsonFile, new JsonExporter().export(shortForm, ExportOptions.builder().build()));
|
||||
|
||||
Path outputDir = tempDir.resolve("out");
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runJsonExporter(jsonFile, outputDir, List.of("json"));
|
||||
|
||||
AnalysisResult roundTripped = new JsonImportService().importAnalysisResult(
|
||||
outputDir.resolve("com.example.config.OrderStateMachineConfiguration")
|
||||
.resolve("com.example.config.OrderStateMachineConfiguration.json"));
|
||||
|
||||
assertThat(roundTripped.getMetadata().getCallChains()).hasSize(1);
|
||||
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getSourceState())
|
||||
.isEqualTo("com.example.order.OrderState.NEW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDedupeStatesByFullIdentifierAfterPropertyResolution(@TempDir Path tempDir) throws Exception {
|
||||
writeSampleProject(tempDir);
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.states(Set.of(
|
||||
State.of("OrderState.NEW", "OrderState.NEW"),
|
||||
State.of("UNDEFINED", "com.example.order.OrderState.NEW")))
|
||||
.build();
|
||||
|
||||
result.applyResolution(Map.of());
|
||||
|
||||
assertThat(result.getStates()).hasSize(1);
|
||||
State only = result.getStates().iterator().next();
|
||||
assertThat(only.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(only.rawName()).isEqualTo("OrderState.NEW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindProjectRootFromJsonDirectory(@TempDir Path tempDir) throws Exception {
|
||||
Path projectRoot = tempDir.resolve("my-app");
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
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.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LegacyDataFlowModelTest {
|
||||
|
||||
@Test
|
||||
void testLegacyDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||
Path comFoo = tempDir.resolve("com/foo");
|
||||
Files.createDirectories(comFoo);
|
||||
Files.writeString(comFoo.resolve("Test.java"),
|
||||
"package com.foo;\n" +
|
||||
"public class Test {\n" +
|
||||
" public void run() {\n" +
|
||||
" String a = \"VAL1\";\n" +
|
||||
" String b = a;\n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||
assertThat(td).isNotNull();
|
||||
|
||||
MethodDeclaration md = td.getMethods()[0];
|
||||
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||
Expression initializer = frag.getInitializer(); // 'a' reference
|
||||
|
||||
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
|
||||
DataFlowModel model = new LegacyDataFlowModel(tracer);
|
||||
|
||||
List<Expression> defs = model.getReachingDefinitions(initializer);
|
||||
assertThat(defs).isNotEmpty();
|
||||
|
||||
Expression resolvedExpr = defs.get(defs.size() - 1);
|
||||
assertThat(resolvedExpr).isInstanceOf(StringLiteral.class);
|
||||
assertThat(((StringLiteral) resolvedExpr).getLiteralValue()).isEqualTo("VAL1");
|
||||
|
||||
String val = model.resolveValue(initializer, context);
|
||||
assertThat(val).isEqualTo("VAL1");
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class ExtendedStateMachineLifecycleHtmlTest {
|
||||
|
||||
String html = Files.readString(htmlFile);
|
||||
assertThat(html).contains("function formatLifecycleLabel");
|
||||
assertThat(html).contains("POST /api/orders/resume");
|
||||
assertThat(html).contains("POST /api/payment/{id}/capture");
|
||||
assertThat(html).contains("Restore persisted state");
|
||||
assertThat(html).contains("[LIFECYCLE:RESTORE]");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package click.kamil.springstatemachineexporter.html;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||
import click.kamil.springstatemachineexporter.html.exporter.HtmlExporter;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||
import click.kamil.springstatemachineexporter.service.JsonImportService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* End-to-end HTML link-key consistency: SVG {@code #link_*} anchors must align with
|
||||
* {@code matchedTransitions} produced by the finalized export pipeline.
|
||||
*/
|
||||
class HtmlTransitionLinkConsistencyTest {
|
||||
|
||||
@Test
|
||||
void jsonReExportHtmlShouldContainLinkKeysForEveryMatchedTransition(@TempDir Path tempDir) throws Exception {
|
||||
writeSampleProject(tempDir);
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.sourceState("OrderState.NEW")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.sourceFile("OrderController.java")
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
||||
.build();
|
||||
CallChain chain = CallChain.builder().triggerPoint(trigger).build();
|
||||
|
||||
AnalysisResult shortForm = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(shortTransition("OrderEvent.PAY", "OrderState.NEW", "OrderState.PAID")))
|
||||
.startStates(Set.of("OrderState.NEW"))
|
||||
.endStates(Set.of("OrderState.PAID"))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.triggers(List.of(trigger))
|
||||
.callChains(List.of(chain))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Path jsonFile = tempDir.resolve("machine.json");
|
||||
Files.writeString(jsonFile, new JsonExporter().export(shortForm, ExportOptions.builder().build()));
|
||||
|
||||
Path outputDir = tempDir.resolve("out");
|
||||
ExportService exportService = new ExportService(List.of(new HtmlExporter(), new JsonExporter()));
|
||||
exportService.runJsonExporter(jsonFile, outputDir, List.of("html", "json"));
|
||||
|
||||
Path machineDir = outputDir.resolve("com.example.config.OrderStateMachineConfiguration");
|
||||
Path htmlFile = machineDir.resolve("com.example.config.OrderStateMachineConfiguration.html");
|
||||
Path finalizedJson = machineDir.resolve("com.example.config.OrderStateMachineConfiguration.json");
|
||||
assertThat(htmlFile).exists();
|
||||
|
||||
String html = Files.readString(htmlFile);
|
||||
AnalysisResult finalized = new JsonImportService().importAnalysisResult(finalizedJson);
|
||||
|
||||
List<MatchedTransition> matched = finalized.getMetadata().getCallChains().stream()
|
||||
.flatMap(c -> c.getMatchedTransitions() == null ? java.util.stream.Stream.empty()
|
||||
: c.getMatchedTransitions().stream())
|
||||
.toList();
|
||||
assertThat(matched).isNotEmpty();
|
||||
|
||||
for (MatchedTransition mt : matched) {
|
||||
String linkKey = buildLinkKey(mt.getSourceState(), mt.getEvent());
|
||||
assertThat(html)
|
||||
.as("HTML must contain SVG link for matched transition %s -> %s", mt.getSourceState(), mt.getEvent())
|
||||
.contains("#link_" + linkKey);
|
||||
}
|
||||
|
||||
assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
|
||||
assertThat(html).contains("com.example.order.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enterpriseAstExportHtmlShouldContainTransitionLinkKeys(@TempDir Path tempDir) throws Exception {
|
||||
Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise");
|
||||
Path outputDir = tempDir.resolve("enterprise-out");
|
||||
|
||||
ExportService exportService = new ExportService(List.of(new HtmlExporter()));
|
||||
exportService.runExporter(enterprise, outputDir, List.of("html"), true, List.of(), null,
|
||||
"OrderStateMachineConfiguration");
|
||||
|
||||
Path machineDir = outputDir.resolve("click.kamil.enterprise.machines.order.OrderStateMachineConfiguration");
|
||||
Path htmlFile = machineDir.resolve("click.kamil.enterprise.machines.order.OrderStateMachineConfiguration.html");
|
||||
assertThat(htmlFile).exists();
|
||||
|
||||
String html = Files.readString(htmlFile);
|
||||
assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
|
||||
assertThat(html).contains("function buildLinkKey(source, event)");
|
||||
assertThat(html).contains("\"matchedTransitions\"");
|
||||
}
|
||||
|
||||
/** Mirrors {@code template.html} link-key derivation for regression checks. */
|
||||
private static String buildLinkKey(String source, String event) {
|
||||
if (event == null || event.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String formattedEvent = formatForLinkId(event);
|
||||
if (source == null || source.isBlank()) {
|
||||
return "__" + normalize(formattedEvent);
|
||||
}
|
||||
return normalize(formatForLinkId(source)) + "__" + normalize(formattedEvent);
|
||||
}
|
||||
|
||||
private static String formatForLinkId(String identifier) {
|
||||
if (identifier == null || identifier.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
int lastDot = identifier.lastIndexOf('.');
|
||||
if (lastDot <= 0) {
|
||||
return identifier;
|
||||
}
|
||||
int prevDot = identifier.lastIndexOf('.', lastDot - 1);
|
||||
return prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
|
||||
}
|
||||
|
||||
private static String normalize(String s) {
|
||||
if (s == null || s.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return s.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
}
|
||||
|
||||
private static Transition shortTransition(String event, String source, String target) {
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of(event, event));
|
||||
transition.setSourceStates(List.of(State.of(source, source)));
|
||||
transition.setTargetStates(List.of(State.of(target, target)));
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static void writeSampleProject(Path projectRoot) throws Exception {
|
||||
Path javaRoot = projectRoot.resolve("src/main/java");
|
||||
Path orderPkg = javaRoot.resolve("com/example/order");
|
||||
Path configPkg = javaRoot.resolve("com/example/config");
|
||||
Files.createDirectories(orderPkg);
|
||||
Files.createDirectories(configPkg);
|
||||
Files.writeString(projectRoot.resolve("build.gradle"), "plugins { id 'java' }");
|
||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
||||
"package com.example.order; public enum OrderState { NEW, PAID }");
|
||||
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
||||
"package com.example.order; public enum OrderEvent { PAY }");
|
||||
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
||||
"""
|
||||
package com.example.config;
|
||||
import com.example.order.OrderEvent;
|
||||
import com.example.order.OrderState;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
public class OrderStateMachineConfiguration
|
||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
Path current = Path.of(".").toAbsolutePath();
|
||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class JsonToHtmlInterchangeTest {
|
||||
// SVG link IDs derive from fn-formatted enum labels; matchedTransitions use package-canonical values.
|
||||
assertThat(html).contains("OrderState_NEW__OrderEvent_PAY");
|
||||
assertThat(html).contains("click.kamil.enterprise.machines.order.OrderEvent.PAY");
|
||||
assertThat(html).contains("POST /api/machine/order/pay");
|
||||
assertThat(html).contains("POST /api/machine/{machineType}/transition/{event}");
|
||||
assertThat(html).contains("\"stateTypeFqn\" : \"click.kamil.enterprise.machines.order.OrderState\"");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user