diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java index 22a914c..daf35bd 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java @@ -1,18 +1,15 @@ 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; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; -import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer; -import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver; import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import lombok.extern.slf4j.Slf4j; +import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; @Slf4j public class CallChainEnricher implements AnalysisEnricher { @@ -21,27 +18,15 @@ public class CallChainEnricher implements AnalysisEnricher { public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { log.info("Enriching {} with call chains", result.getName()); - StateMachineTypeResolver.MachineTypes machineTypes = - StateMachineTypeResolver.resolveTypes(result.getName(), context); - - List canonicalTriggers = intelligence.findTriggerPoints().stream() - .map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes)) - .collect(Collectors.toList()); - List scopedTriggers = MachineScopeFilter.filterTriggersForMachine( - canonicalTriggers, result.getName(), context); + List scopedTriggers = result.getMetadata() != null + && result.getMetadata().getTriggers() != null + ? result.getMetadata().getTriggers() + : Collections.emptyList(); List chains = intelligence.findCallChains( result.getMetadata().getEntryPoints(), scopedTriggers ); - chains = chains.stream() - .map(chain -> chain.getTriggerPoint() == null - ? chain - : chain.toBuilder() - .triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint( - chain.getTriggerPoint(), machineTypes)) - .build()) - .collect(Collectors.toList()); chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context); result.addMetadata(CodebaseMetadata.builder() diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java index a4ec26b..677b8cf 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java @@ -11,7 +11,6 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.stream.Collectors; /** * Canonicalizes trigger-side enum identifiers ({@code event}, {@code polymorphicEvents}, @@ -30,10 +29,6 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher { StateMachineTypeResolver.resolveTypes(result.getName(), context); List triggers = result.getMetadata().getTriggers(); - List canonicalTriggers = triggers == null ? null : triggers.stream() - .map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes)) - .collect(Collectors.toList()); - List callChains = result.getMetadata().getCallChains(); List canonicalChains = callChains == null ? null : callChains.stream() .map(chain -> { @@ -45,16 +40,16 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher { chain.getTriggerPoint(), machineTypes)) .build(); }) - .collect(Collectors.toList()); + .toList(); - if (canonicalTriggers == null && canonicalChains == null) { + if (canonicalChains == null) { return; } log.debug("Canonicalized triggers for {}", result.getName()); result.setMetadata(CodebaseMetadata.builder() - .triggers(canonicalTriggers != null ? canonicalTriggers : triggers) + .triggers(triggers) .entryPoints(result.getMetadata().getEntryPoints()) .callChains(canonicalChains != null ? canonicalChains : callChains) .properties(result.getMetadata().getProperties()) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/index/AccessorNaming.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/index/AccessorNaming.java new file mode 100644 index 0000000..cfd8cc1 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/index/AccessorNaming.java @@ -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"); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/AccessorResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/AccessorResolver.java index cca9bad..e3ad155 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/AccessorResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/AccessorResolver.java @@ -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 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 constants = new ArrayList<>(); - BiConsumer> 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) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/ClassCompatibilityCache.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/ClassCompatibilityCache.java index 047adc9..32d8284 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/ClassCompatibilityCache.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/ClassCompatibilityCache.java @@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; public final class ClassCompatibilityCache { private final Map, Boolean> cache = new ConcurrentHashMap<>(); + private final Map 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 memoize(String key, java.util.function.Function computer) { + return (T) memoCache.computeIfAbsent(key, computer); + } + + public void clearMemoCache() { + memoCache.clear(); + } + public void clear() { cache.clear(); + memoCache.clear(); } private static Map.Entry cacheKey(String classNeighbor, String classTarget) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/GetterBodyConstantScanner.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/GetterBodyConstantScanner.java new file mode 100644 index 0000000..e96c430 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/pipeline/GetterBodyConstantScanner.java @@ -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 extractConstantGetterReturn( + CodebaseContext context, + ConstantExtractor constantExtractor, + ConstantResolver constantResolver, + String ownerFqn, + String methodName) { + return extractConstantGetterReturn(context, constantExtractor, constantResolver, ownerFqn, methodName, null, null); + } + + public static List extractConstantGetterReturn( + CodebaseContext context, + ConstantExtractor constantExtractor, + ConstantResolver constantResolver, + String ownerFqn, + String methodName, + CompilationUnit contextCu, + Set 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 constants = new ArrayList<>(); + BiConsumer> 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; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index 29274ae..146ff22 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -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) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/StateMachineTypeResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/StateMachineTypeResolver.java index c184373..d342816 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/StateMachineTypeResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/StateMachineTypeResolver.java @@ -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 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); } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index 6d83539..10c8cbd 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -33,6 +33,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { protected final PathBindingEvaluator pathBindingEvaluator; private final Map parsedNodeCache = new HashMap<>(); private final Map> polymorphicCallCache = new HashMap<>(); + protected Map> 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 values = constantExtractor.resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu); + List 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 values = constantExtractor.resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu); + List 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 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 constants = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse, ResolutionBudget.defaults(), false); + List 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 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 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 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> buildCallGraph(); + protected abstract String callGraphCacheKey(); + protected abstract String resolveCalledMethod(MethodInvocation node); + @SuppressWarnings("unchecked") + protected Map> buildCallGraph() { + Map> cached = + (Map>) 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 calledMethods = resolveCalledMethodsPolymorphic(node); + List 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 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 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 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 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 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 constants = constantExtractor.resolveMethodReturnConstant( - ownerFqn, methodName, 0, new HashSet<>(), cu); + List constants = resolveAccessorConstants(ownerFqn, methodName, cu); if (constants.isEmpty()) { return null; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java deleted file mode 100644 index 3704898..0000000 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java +++ /dev/null @@ -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> graph; - - @lombok.Data - private static class CallEdge { - private final String targetMethod; - private final List arguments; - } - - public CallGraphBuilder(CodebaseContext context) { - this.context = context; - this.constantResolver = new ConstantResolver(); - } - - public List findChains(List entryPoints, List triggers) { - Map> callGraph = buildCallGraph(); - List 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 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 path, Map> 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 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 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 typesToInspect = new ArrayList<>(); - typesToInspect.add(declaredType); - typesToInspect.addAll(context.getImplementations(declaredType)); - - for (String type : typesToInspect) { - Set 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 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 resolveMethodReturnConstant(String className, String methodName, int depth, Set visited, CompilationUnit contextCu) { - if (depth > 20) return Collections.emptyList(); - String fqn = className + "." + methodName; - if (!visited.add(fqn)) return Collections.emptyList(); - - List 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 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 path, Map> callGraph) { - for (String node : path) { - List 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> 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 calledMethods = resolveCalledMethodsPolymorphic(node); - List 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 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 args = resolveArguments(node.arguments()); - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); - } - } - } - } - return super.visit(node); - } - }); - } - return graph; - } - - private List resolveArguments(List astArguments) { - List 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 resolveCalledMethodsPolymorphic(MethodInvocation node) { - String baseCalled = resolveCalledMethod(node); - if (baseCalled == null) return Collections.emptyList(); - - List allResolved = new ArrayList<>(); - allResolved.add(baseCalled); - - if (baseCalled.contains(".")) { - String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); - String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); - - List 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 findPath(String start, String target, Map> graph, Set visited) { - if (start.equals(target)) return new ArrayList<>(List.of(start)); - if (!visited.add(start)) return null; // Path-scoped cycle detection - - List 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 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; - } -} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java index befda38..016aace 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java @@ -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 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 implementations = context.getImplementations(className); - if (implementations == null || implementations.isEmpty()) { - return Optional.empty(); - } - Optional precomputed = context.getPolymorphicAccessorIndex() - .representativeImplAccessor(className, methodName); - if (precomputed.isPresent()) { - return precomputed; - } - for (String implementation : implementations) { - Optional implAccessor = context.getAccessorIndex().lookup(implementation, methodName); - if (implAccessor.isPresent()) { - return implAccessor; - } - } - return Optional.empty(); + return context.getAccessorIndex().lookup(className, methodName); } private List extractConstantGetterReturn( @@ -568,30 +540,8 @@ public class ConstantExtractor { String methodName, CompilationUnit contextCu, Set 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 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); } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java index e463ceb..b3fdc88 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java @@ -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); } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java index f78eb2d..3299c74 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java @@ -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 hints; + private final TypeResolver typeResolver; + + public GenericEventDetector(CodebaseContext context, ConstantResolver constantResolver, List 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 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 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 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> assignments = new java.util.HashMap<>(); final java.util.Set parameters = new java.util.HashSet<>(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index e6bc835..ce1509a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -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> graph; public HeuristicCallGraphEngine(CodebaseContext context) { super(context); } - @SuppressWarnings("unchecked") - protected Map> buildCallGraph() { - Map> cached = (Map>) 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 calledMethods = resolveCalledMethodsPolymorphic(node); - List 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 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 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 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 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; - } -} \ No newline at end of file +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index 31cf55b..a7bb4ab 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -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> graph; public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) { super(context); this.injectionAnalyzer = injectionAnalyzer; } - @SuppressWarnings("unchecked") - protected Map> buildCallGraph() { - Map> cached = (Map>) 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 calledMethods = resolveCalledMethodsPolymorphic(node); - List 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 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 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 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 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; - } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java index 778470b..ce1a344 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java @@ -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; - } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringComponentDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringComponentDetector.java index 9f3db68..6337d45 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringComponentDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringComponentDetector.java @@ -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 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 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 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 ""; - } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java index ec2d1a0..157ecfa 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java @@ -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 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); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java index 8da5b7b..674a49f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java @@ -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 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 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)); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/spring/SpringDependencyResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/spring/SpringDependencyResolver.java index 7215ee0..5b0fbf7 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/spring/SpringDependencyResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/spring/SpringDependencyResolver.java @@ -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; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java index e79c918..1d47bc6 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java @@ -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)); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/AstUtils.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/AstUtils.java index 5c6e5f1..d30e5bd 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/AstUtils.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/AstUtils.java @@ -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(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java index 043a62d..65942a9 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java @@ -14,7 +14,6 @@ public class JdtDataFlowModel implements DataFlowModel { private final Map cfgCache = new HashMap<>(); private final Map rdCache = new HashMap<>(); private final Map> objectFieldBindings = new HashMap<>(); - private final Map> compatibleContextTypesCache = new HashMap<>(); private static final ThreadLocal> 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 getCompatibleContextTypes(List 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)); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModel.java deleted file mode 100644 index c863946..0000000 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModel.java +++ /dev/null @@ -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 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); - } -} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/StateResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/StateResolver.java index 2123594..5d1f69b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/StateResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/StateResolver.java @@ -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(); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java deleted file mode 100644 index f706c40..0000000 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java +++ /dev/null @@ -1,1125 +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 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 CallGraphBuilderTest { - - @Test - void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException { - String source = """ - package com.example; - public class OrderService { - private final EventDispatcher dispatcher = new EventDispatcher(); - - public void processOrder() { - dispatcher.dispatch("order", this::sendEvent); - } - - public void sendEvent(String event) { - // Dummy trigger - } - } - - class EventDispatcher { - public void dispatch(String order, java.util.function.Consumer callback) { - callback.accept("MY_EVENT"); - } - } - """; - Files.writeString(tempDir.resolve("OrderService.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderService") - .methodName("processOrder") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("sendEvent") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getMethodChain()).containsExactly( - "com.example.OrderService.processOrder", - "com.example.OrderService.sendEvent" - ); - } - - @Test - void shouldExtractContextMachineId(@TempDir Path tempDir) throws IOException { - String source = """ - package com.example; - public class PersisterService { - private final StateMachinePersister persister = new StateMachinePersister(); - - public void updateOrderState(String orderId) { - StateMachine sm = persister.restore(null, "machine:" + orderId); - sm.sendEvent("PAY"); - } - } - - class StateMachinePersister { - public StateMachine restore(Object sm, String contextObj) { - return new StateMachine(); - } - } - - class StateMachine { - public void sendEvent(String event) {} - } - """; - Files.writeString(tempDir.resolve("PersisterService.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.PersisterService") - .methodName("updateOrderState") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.StateMachine") - .methodName("sendEvent") - .event("PAY") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getContextMachineId()).isNotNull(); - // Since it's tracing constant concatenation: "machine:" + orderId, which might resolve dynamically or string format. - // It should at least be present (it typically extracts 'machine:' + orderId into expression). - } - - @Test - void shouldResolveTriggerPointParametersAcrossChain() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void createOrder() { - service.updateOrderState(OrderEvents.CREATE); - } - } - - class OrderService { - private StateMachine sm; - public void updateOrderState(OrderEvents event) { - sm.sendEvent(event); - } - } - - class StateMachine { - public void sendEvent(Object event) {} - } - - enum OrderEvents { CREATE, PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("createOrder") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CREATE"); - } - - @Test - void shouldResolveTriggerPointLocalVariableAcrossChain() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(MyOrderEvent domainEvent) { - doProcessOrderEvent(domainEvent); - } - - private void doProcessOrderEvent(MyOrderEvent domainEvent) { - OrderEvents eventType = domainEvent.getType(); - service.updateOrderState(eventType); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) { - // sendEvent - } - } - - class MyOrderEvent { - public OrderEvents getType() { return OrderEvents.PAY; } - } - - enum OrderEvents { CREATE, PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test2"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) - .containsExactlyInAnyOrder("OrderEvents.PAY"); - } - - @Test - void shouldResolvePolymorphicInheritanceAcrossChain() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(RichOrderEvent domainEvent) { - doProcessOrderEvent(domainEvent); - } - - private void doProcessOrderEvent(RichOrderEvent domainEvent) { - OrderEvents eventType = assertSupportedOrderEvent(domainEvent.getType()); - service.updateOrderState(eventType); - } - - private OrderEvents assertSupportedOrderEvent(OrderEvents e) { - return e; - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) { - // sendEvent - } - } - - interface RichOrderEvent { - OrderEvents getType(); - } - - class CancelOrderEvent implements RichOrderEvent { - public OrderEvents getType() { return OrderEvents.CANCELLED; } - } - - class ReceiveOrderEvent implements RichOrderEvent { - public OrderEvents getType() { return OrderEvents.RECEIVED; } - } - - enum OrderEvents { CREATE, PAY, CANCELLED, RECEIVED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_poly"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) - .containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED"); - } - - @Test - void shouldUnwrapDeepMethodWrappers() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(RichOrderEvent domainEvent) { - OrderEvents eventType = wrap3(wrap2(wrap1(domainEvent.getType()))); - service.updateOrderState(eventType); - } - - private OrderEvents wrap1(OrderEvents e) { return e; } - private OrderEvents wrap2(OrderEvents e) { return e; } - private OrderEvents wrap3(OrderEvents e) { return e; } - } - - class OrderService { - public void updateOrderState(OrderEvents event) { } - } - - class RichOrderEvent { - public OrderEvents getType() { return OrderEvents.CREATE; } - } - - enum OrderEvents { CREATE } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_deep_wrap"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) - .containsExactlyInAnyOrder("OrderEvents.CREATE"); - } - - @Test - void shouldResolvePolymorphicInheritanceThroughDelegation() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(BaseEvent domainEvent) { - service.updateOrderState(domainEvent.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - interface BaseEvent { - OrderEvents getType(); - } - - abstract class AbstractEvent implements BaseEvent { - protected OrderEvents internalGetType() { - return OrderEvents.PAY; - } - } - - class ConcreteEvent extends AbstractEvent { - public OrderEvents getType() { - return internalGetType(); - } - } - - class AnotherConcreteEvent implements BaseEvent { - public OrderEvents getType() { - return delegateToHelper(); - } - private OrderEvents delegateToHelper() { - return OrderEvents.CANCELLED; - } - } - - enum OrderEvents { PAY, CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_poly_delegation"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED"); - } - - @Test - void shouldHandleMissingImplementationsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(UnknownEvent domainEvent) { - service.updateOrderState(domainEvent.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - // UnknownEvent is not defined in the source (e.g. from a third-party library) - - enum OrderEvents { CREATE } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_missing_impl"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty(); - } - - @Test - void shouldTraceMultipleLocalVariableAssignments() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(RichOrderEvent domainEvent) { - OrderEvents type1 = domainEvent.getType(); - OrderEvents type2 = type1; - OrderEvents type3 = type2; - service.updateOrderState(type3); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - class RichOrderEvent { - public OrderEvents getType() { return OrderEvents.RECEIVED; } - } - - enum OrderEvents { RECEIVED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_multiple_assign"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .containsExactlyInAnyOrder("OrderEvents.RECEIVED"); - } - - @Test - void shouldResolveMethodInheritedFromAbstractBaseClass() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(BaseEvent domainEvent) { - service.updateOrderState(domainEvent.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - interface BaseEvent { - OrderEvents getType(); - } - - abstract class AbstractEvent implements BaseEvent { - public OrderEvents getType() { - return OrderEvents.PAY; - } - } - - // Concrete subclass inherits getType() but doesn't override it! - class InheritingEvent extends AbstractEvent { - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_inherited"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .containsExactlyInAnyOrder("OrderEvents.PAY"); - } - - @Test - void shouldStopTracingRecursiveMethodsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(RecursiveEvent domainEvent) { - service.updateOrderState(domainEvent.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - class RecursiveEvent { - public OrderEvents getType() { - return this.getType(); - } - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_recursive"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // Should safely complete without stack overflow and return an empty result - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isEmpty(); - } - - @Test - void shouldHandleChainedMethodCallsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(EventWrapper wrapper) { - service.updateOrderState(wrapper.getEvent().getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - class EventWrapper { - public RichEvent getEvent() { return new RichEvent(); } - } - - class RichEvent { - public OrderEvents getType() { return OrderEvents.PAY; } - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_chained"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // It might not fully resolve chained method calls, but it must not crash! - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("wrapper.getEvent().getType()"); - } - - @Test - void shouldTraceStaticFieldReferences() throws IOException { - String source = """ - package com.example; - public class OrderController { - private static final OrderEvents CONSTANT_EVENT = OrderEvents.CANCELLED; - private OrderService service; - public void processOrderEvent() { - service.updateOrderState(CONSTANT_EVENT); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_static_field"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED"); - } - - @Test - void shouldHandleAnonymousInnerClassesGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - RichEvent event = new RichEvent() { - public OrderEvents getType() { - return OrderEvents.CREATE; - } - }; - service.updateOrderState(event.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - interface RichEvent { - OrderEvents getType(); - } - - enum OrderEvents { CREATE } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_anonymous"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // It successfully traces the local variable 'event' to its anonymous class initializer - assertThat(chain.getTriggerPoint().getEvent()).startsWith("new RichEvent(){"); - assertThat(chain.getTriggerPoint().getEvent()).endsWith(".getType()"); - } - - @Test - void shouldLinkCallsInsideLambdasToEnclosingMethod() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - Runnable r = () -> service.updateOrderState(OrderEvents.PAY); - r.run(); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_lambda_scope"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.PAY"); - } - - @Test - void shouldHandleTernaryOperatorsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(boolean flag) { - OrderEvents event = flag ? OrderEvents.PAY : OrderEvents.CANCELLED; - service.updateOrderState(event); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY, CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_ternary"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // It should gracefully fallback to the string representation of the ternary operator - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("flag ? OrderEvents.PAY : OrderEvents.CANCELLED"); - } - - @Test - void shouldTraceLastTextualAssignmentInTryCatchBlocks() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - OrderEvents event; - try { - event = OrderEvents.PAY; - } catch (Exception e) { - event = OrderEvents.CANCELLED; - } - service.updateOrderState(event); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY, CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_try_catch"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // The ASTVisitor picks up Assignments in textual order. - // CANCELLED is the last one textually in the method. - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED"); - } - - @Test - void shouldTraceArrayAndCollectionAccessGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY}; - service.updateOrderState(events[0]); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_array"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.OrderController") - .methodName("processOrderEvent") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // We don't dynamically resolve array indexes, but it shouldn't crash. - // It returns the array access expression as a string. - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]"); - } - - @Test - void shouldProperlyUnwrapDeeplyNestedWrapperCallsForFix1() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(MyEvent event) { - OrderEvents eventType = wrap1(wrap2(wrap3(event.getType()))); - service.updateOrderState(eventType); - } - - private OrderEvents wrap1(OrderEvents e) { return e; } - private OrderEvents wrap2(OrderEvents e) { return e; } - private OrderEvents wrap3(OrderEvents e) { return e; } - } - - class OrderService { - public void updateOrderState(OrderEvents event) { } - } - - class MyEvent { - public OrderEvents getType() { return OrderEvents.CREATE; } - } - enum OrderEvents { CREATE } - """; - Path tempDir = Files.createTempDirectory("callgraph_nested_wrap"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("processOrderEvent").build(); - TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("updateOrderState").event("event").build(); - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - assertThat(chains).hasSize(1); - assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.CREATE"); - } - - @Test - void shouldNotExtractClassInstanceCreationEvenWithComplexArgsForFix2() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void createOrder(String oID) { - service.processOrderEvent(new OrderCancelledEvent(new OrderId(oID))); - } - } - class OrderService { - public void processOrderEvent(OrderCancelledEvent event) { } - } - class OrderCancelledEvent { - public OrderCancelledEvent(OrderId id) {} - } - class OrderId { - public OrderId(String id) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_complex_cic"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("createOrder").build(); - TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build(); - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - assertThat(chains).hasSize(1); - assertThat(chains.get(0).getTriggerPoint().getEvent()).isEqualTo("new OrderCancelledEvent(new OrderId(oID))"); - } - - @Test - void shouldResolveMethodReturnConstantFromMultiplePackagesForFix3() throws IOException { - String source1 = """ - package com.example.api; - public interface RichEvent { - String getType(); - } - """; - String source2 = """ - package com.example.impl.a; - import com.example.api.RichEvent; - public class EventA implements RichEvent { - public String getType() { return "EVENT_A"; } - } - """; - String source3 = """ - package com.example.impl.b; - import com.example.api.RichEvent; - public class EventB implements RichEvent { - public String getType() { return "EVENT_B"; } - } - """; - String source4 = """ - package com.example.controller; - import com.example.api.RichEvent; - public class EventController { - private com.example.service.EventService service; - public void handleEvent(RichEvent event) { - service.process(event.getType()); - } - } - """; - String source5 = """ - package com.example.service; - public class EventService { - public void process(String type) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_multi_pkg"); - Files.writeString(tempDir.resolve("RichEvent.java"), source1); - Files.writeString(tempDir.resolve("EventA.java"), source2); - Files.writeString(tempDir.resolve("EventB.java"), source3); - Files.writeString(tempDir.resolve("EventController.java"), source4); - Files.writeString(tempDir.resolve("EventService.java"), source5); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build(); - TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build(); - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - assertThat(chains).hasSize(1); - assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B"); - } - - @Test - void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void handleStatus(DomainEvent domainEvent) { - service.process(domainEvent.getEventForStatus()); - } - } - - class DomainEvent { - private OrderStatus status; - public String getEventForStatus() { - switch (status) { - case NEW: return "CREATE_EVENT"; - case PAID: return "PAY_EVENT"; - default: return "CANCEL_EVENT"; - } - } - } - - class OrderService { - public void process(String event) {} - } - - enum OrderStatus { NEW, PAID } - """; - Path tempDir = Files.createTempDirectory("callgraph_switch"); - Files.writeString(tempDir.resolve("OrderConfig.java"), source); - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - CallGraphBuilder builder = new CallGraphBuilder(context); - - EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); - TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - assertThat(chains).hasSize(1); - assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT"); - } -} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModelTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModelTest.java deleted file mode 100644 index dfdfe43..0000000 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/LegacyDataFlowModelTest.java +++ /dev/null @@ -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 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"); - } -}