Compare commits
7 Commits
still_miss
...
ff16c1bbe7
| Author | SHA1 | Date | |
|---|---|---|---|
| ff16c1bbe7 | |||
| f9e6247eb3 | |||
| 76ac143370 | |||
| 9ca8b2fe37 | |||
| 56cdf96f2d | |||
| c37aa92c78 | |||
| 3ac057618f |
@@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) {
|
||||
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent)))) {
|
||||
// Event matches or is a wildcard
|
||||
for (State smSourceState : t.getSourceStates()) {
|
||||
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
||||
@@ -123,17 +123,19 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
result.setMetadata(updatedMetadata);
|
||||
}
|
||||
|
||||
|
||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||
// If the chain's target expression or qualifier indicates a specific machine name, verify it
|
||||
// First, check explicit variable targeting (like contextMachineId)
|
||||
String targetVar = chain.getContextMachineId();
|
||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
|
||||
String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : "";
|
||||
|
||||
if (targetVar != null && !targetVar.isEmpty()) {
|
||||
targetVar = targetVar.toLowerCase();
|
||||
// E.g., if target is "myStateMachine", ensure current machine name contains "my"
|
||||
String simplifiedMachineName = currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase();
|
||||
// If the variable name ends with StateMachine, we extract the prefix
|
||||
if (targetVar.endsWith("statemachine")) {
|
||||
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
|
||||
@@ -142,10 +144,161 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Generic approach combining Prefix, Package, and String-distance heuristics
|
||||
// without relying on a hardcoded list of magic keywords.
|
||||
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
|
||||
String smPackage = getPackageName(currentMachineName);
|
||||
String smSimple = getSimpleClassName(currentMachineName);
|
||||
String smPrefix = getFirstCamelCaseWord(smSimple);
|
||||
|
||||
boolean hasPositiveMatch = false;
|
||||
boolean hasStrongMismatch = false;
|
||||
|
||||
for (String method : chain.getMethodChain()) {
|
||||
String chainClass = getClassNameOnly(method);
|
||||
String chainPackage = getPackageName(chainClass);
|
||||
String chainSimple = getSimpleClassName(chainClass);
|
||||
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||
|
||||
// 1. Explicit Prefix Match (e.g. ElectronicsOrderService & ElectronicsStateMachine)
|
||||
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
||||
hasPositiveMatch = true;
|
||||
}
|
||||
|
||||
// 2. Explicit Sub-Package Match (e.g. com.company.electronics.service & com.company.electronics.sm)
|
||||
// We consider it a match if they share a package segment beyond the first 2 (which are usually com.company)
|
||||
String deepShared = getDeepSharedPackage(smPackage, chainPackage);
|
||||
if (deepShared != null) {
|
||||
hasPositiveMatch = true;
|
||||
}
|
||||
|
||||
// Determine if there is a strong mismatch.
|
||||
// A strong mismatch happens if the chain class has a VERY distinct prefix/package
|
||||
// that contradicts the SM's distinct prefix/package.
|
||||
// Without magic keywords, we guess if a prefix is distinct if it's long enough and doesn't match.
|
||||
if (smPrefix != null && chainPrefix != null && !smPrefix.equals(chainPrefix)) {
|
||||
// They have different first CamelCase words.
|
||||
// This could be Electronics vs ComputerStore, or it could be Common vs ComputerStore.
|
||||
// If they diverge at a deep package level, it's a strong mismatch.
|
||||
if (isDeepPackageDivergence(smPackage, chainPackage)) {
|
||||
hasStrongMismatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a positive structural match, route it!
|
||||
if (hasPositiveMatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there's no positive match, but we detected a strong structural mismatch, reject it!
|
||||
if (hasStrongMismatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If neither, we allow it (fallback for common/shared endpoints without distinct domains)
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
private String getPackageName(String fqn) {
|
||||
if (fqn == null || !fqn.contains(".")) return "";
|
||||
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
private String getSimpleClassName(String fqn) {
|
||||
if (fqn == null) return "";
|
||||
if (!fqn.contains(".")) return fqn;
|
||||
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
private String getClassNameOnly(String methodFqn) {
|
||||
if (methodFqn == null) return "";
|
||||
String clean = methodFqn;
|
||||
if (clean.contains("(")) {
|
||||
clean = clean.substring(0, clean.indexOf('('));
|
||||
}
|
||||
if (clean.contains(".")) {
|
||||
clean = clean.substring(0, clean.lastIndexOf('.'));
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
private String getFirstCamelCaseWord(String simpleName) {
|
||||
if (simpleName == null || simpleName.isEmpty()) return null;
|
||||
// e.g. "ElectronicsOrderService" -> "Electronics"
|
||||
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
|
||||
if (words.length > 0) {
|
||||
return words[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDomainRootPackage(String[] p) {
|
||||
if (p.length > 0 && (p[p.length - 1].equals("config") || p[p.length - 1].equals("configuration"))) {
|
||||
return String.join(".", java.util.Arrays.copyOf(p, p.length - 1));
|
||||
}
|
||||
return String.join(".", p);
|
||||
}
|
||||
|
||||
private String getDeepSharedPackage(String pkg1, String pkg2) {
|
||||
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
|
||||
|
||||
String domain1 = getDomainRootPackage(pkg1.split("\\."));
|
||||
String[] p1 = domain1.split("\\.");
|
||||
String[] p2 = pkg2.split("\\.");
|
||||
|
||||
int matchIdx = -1;
|
||||
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||
if (p1[i].equals(p2[i])) {
|
||||
matchIdx = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Require matching up to the domain root
|
||||
int requiredMatchIdx = Math.max(1, p1.length - 1);
|
||||
if (matchIdx >= requiredMatchIdx) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i = 0; i <= matchIdx; i++) {
|
||||
sb.append(p1[i]).append(".");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isDeepPackageDivergence(String pkg1, String pkg2) {
|
||||
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false;
|
||||
|
||||
String domain1 = getDomainRootPackage(pkg1.split("\\."));
|
||||
String[] p1 = domain1.split("\\.");
|
||||
String[] p2 = pkg2.split("\\.");
|
||||
|
||||
int matchIdx = -1;
|
||||
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||
if (p1[i].equals(p2[i])) {
|
||||
matchIdx = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic threshold based on domain root
|
||||
int requiredBaseMatchIdx = p1.length - 2;
|
||||
if (requiredBaseMatchIdx >= 1 && matchIdx == requiredBaseMatchIdx) {
|
||||
return p2.length > matchIdx + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
if (name == null) return null;
|
||||
// Strip common suffixes
|
||||
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||
@@ -157,4 +310,31 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
|
||||
return simplified;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||
// Only fire if it looks like a simple identifier (no spaces, no parens)
|
||||
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
|
||||
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
|
||||
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
|
||||
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
|
||||
|
||||
shorter = shorter.toLowerCase();
|
||||
longer = longer.toLowerCase();
|
||||
|
||||
if (longer.contains(shorter)) {
|
||||
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
|
||||
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
|
||||
if (shorter.length() < 4) {
|
||||
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*");
|
||||
}
|
||||
// For longer strings, a straight contains is usually safe enough
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class BeanDefinition {
|
||||
private String name;
|
||||
private String typeFqn;
|
||||
private List<String> qualifiers;
|
||||
private boolean isPrimary;
|
||||
private TypeDeclaration declaringClass;
|
||||
private MethodDeclaration declaringMethod;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class BeanRegistry {
|
||||
private final CodebaseContext context;
|
||||
private final List<BeanDefinition> beans = new ArrayList<>();
|
||||
|
||||
public void register(BeanDefinition bean) {
|
||||
beans.add(bean);
|
||||
}
|
||||
|
||||
public List<BeanDefinition> getBeans() {
|
||||
return beans;
|
||||
}
|
||||
|
||||
public BeanDefinition resolve(String expectedTypeFqn, String expectedName, String expectedQualifier) {
|
||||
System.out.println("RESOLVING: type=" + expectedTypeFqn + " name=" + expectedName + " qual=" + expectedQualifier);
|
||||
// 1. Qualifier Match
|
||||
if (expectedQualifier != null && !expectedQualifier.isEmpty()) {
|
||||
for (BeanDefinition bean : beans) {
|
||||
if (matchesType(bean.getTypeFqn(), expectedTypeFqn) && bean.getQualifiers().contains(expectedQualifier)) {
|
||||
System.out.println(" -> Matched by qualifier: " + bean.getName());
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<BeanDefinition> typeMatches = new ArrayList<>();
|
||||
for (BeanDefinition bean : beans) {
|
||||
System.out.println(" Checking bean: " + bean.getName() + " of type " + bean.getTypeFqn());
|
||||
if (matchesType(bean.getTypeFqn(), expectedTypeFqn)) {
|
||||
typeMatches.add(bean);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeMatches.isEmpty()) {
|
||||
System.out.println(" -> No type matches found");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeMatches.size() == 1) {
|
||||
System.out.println(" -> Single type match: " + typeMatches.get(0).getName());
|
||||
return typeMatches.get(0);
|
||||
}
|
||||
|
||||
// 2. Name Match
|
||||
if (expectedName != null && !expectedName.isEmpty()) {
|
||||
for (BeanDefinition bean : typeMatches) {
|
||||
if (expectedName.equals(bean.getName())) {
|
||||
System.out.println(" -> Matched by name: " + bean.getName());
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Primary Match
|
||||
for (BeanDefinition bean : typeMatches) {
|
||||
if (bean.isPrimary()) {
|
||||
System.out.println(" -> Matched by primary: " + bean.getName());
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback
|
||||
System.out.println(" -> Fallback (first match): " + typeMatches.get(0).getName());
|
||||
return typeMatches.get(0);
|
||||
}
|
||||
|
||||
private boolean matchesType(String actualTypeFqn, String expectedTypeFqn) {
|
||||
if (actualTypeFqn == null || expectedTypeFqn == null) return false;
|
||||
if (actualTypeFqn.equals(expectedTypeFqn)) return true;
|
||||
|
||||
// Check if actualTypeFqn implements expectedTypeFqn
|
||||
List<String> actualImpls = context.getImplementations(expectedTypeFqn);
|
||||
return actualImpls.contains(actualTypeFqn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BeanScanner {
|
||||
|
||||
public static void scanAndRegister(CompilationUnit cu, CodebaseContext context, BeanRegistry registry) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(TypeDeclaration node) {
|
||||
boolean isComponent = false;
|
||||
boolean isPrimary = false;
|
||||
String beanName = null;
|
||||
List<String> qualifiers = new ArrayList<>();
|
||||
|
||||
for (Object modifier : node.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation) {
|
||||
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if (typeName.endsWith("Component") || typeName.endsWith("Service") ||
|
||||
typeName.endsWith("Repository") || typeName.endsWith("Controller") ||
|
||||
typeName.endsWith("Configuration")) {
|
||||
isComponent = true;
|
||||
String value = extractAnnotationValue(annotation, "value");
|
||||
if (!value.isEmpty()) {
|
||||
beanName = value.replace("\"", "");
|
||||
}
|
||||
} else if (typeName.endsWith("Primary")) {
|
||||
isPrimary = true;
|
||||
} else if (typeName.endsWith("Qualifier")) {
|
||||
String value = extractAnnotationValue(annotation, "value");
|
||||
if (!value.isEmpty()) {
|
||||
qualifiers.add(value.replace("\"", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isComponent) {
|
||||
if (beanName == null) {
|
||||
beanName = decapitalize(node.getName().getIdentifier());
|
||||
}
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name(beanName)
|
||||
.typeFqn(context.getFqn(node))
|
||||
.qualifiers(qualifiers)
|
||||
.isPrimary(isPrimary)
|
||||
.declaringClass(node)
|
||||
.build());
|
||||
}
|
||||
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
if (!(node.getParent() instanceof TypeDeclaration parentTd)) {
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
boolean isBean = false;
|
||||
boolean isPrimary = false;
|
||||
String beanName = null;
|
||||
List<String> qualifiers = new ArrayList<>();
|
||||
|
||||
for (Object modifier : node.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation) {
|
||||
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if (typeName.endsWith("Bean")) {
|
||||
isBean = true;
|
||||
String value = extractAnnotationValue(annotation, "value");
|
||||
if (value.isEmpty()) {
|
||||
value = extractAnnotationValue(annotation, "name");
|
||||
}
|
||||
if (!value.isEmpty()) {
|
||||
beanName = value.replace("\"", "");
|
||||
}
|
||||
} else if (typeName.endsWith("Primary")) {
|
||||
isPrimary = true;
|
||||
} else if (typeName.endsWith("Qualifier")) {
|
||||
String value = extractAnnotationValue(annotation, "value");
|
||||
if (!value.isEmpty()) {
|
||||
qualifiers.add(value.replace("\"", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isBean && node.getReturnType2() != null) {
|
||||
if (beanName == null) {
|
||||
beanName = node.getName().getIdentifier();
|
||||
}
|
||||
String returnType = extractTypeName(node.getReturnType2());
|
||||
// We need the FQN of the return type. Try resolving it using context.
|
||||
String returnTypeFqn = resolveFqn(returnType, parentTd, context);
|
||||
if (returnTypeFqn == null) {
|
||||
returnTypeFqn = returnType; // fallback
|
||||
}
|
||||
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name(beanName)
|
||||
.typeFqn(returnTypeFqn)
|
||||
.qualifiers(qualifiers)
|
||||
.isPrimary(isPrimary)
|
||||
.declaringClass(parentTd)
|
||||
.declaringMethod(node)
|
||||
.build());
|
||||
}
|
||||
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String extractAnnotationValue(Annotation annotation, String memberName) {
|
||||
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||
if ("value".equals(memberName) || "name".equals(memberName)) { // @Bean("name") uses SingleMemberAnnotation with value()
|
||||
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 "";
|
||||
}
|
||||
|
||||
private static String decapitalize(String name) {
|
||||
if (name == null || name.isEmpty()) return name;
|
||||
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) {
|
||||
return name;
|
||||
}
|
||||
char[] chars = name.toCharArray();
|
||||
chars[0] = Character.toLowerCase(chars[0]);
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
private static String extractTypeName(Type type) {
|
||||
if (type.isSimpleType()) {
|
||||
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return extractTypeName(((ParameterizedType) type).getType());
|
||||
} else if (type.isArrayType()) {
|
||||
return extractTypeName(((ArrayType) type).getElementType());
|
||||
}
|
||||
return type.toString();
|
||||
}
|
||||
|
||||
private static String resolveFqn(String typeName, TypeDeclaration contextTd, CodebaseContext context) {
|
||||
CompilationUnit cu = (CompilationUnit) contextTd.getRoot();
|
||||
TypeDeclaration td = context.getTypeDeclaration(typeName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
// Fallback to imports
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + typeName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,26 @@ public class ConstantResolver {
|
||||
return resolveManual(sn, context, visited);
|
||||
}
|
||||
|
||||
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
|
||||
String val = resolveInternal(mi.getExpression(), context, visited);
|
||||
if (val != null) {
|
||||
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
|
||||
if (val.contains(".")) {
|
||||
return val.substring(val.lastIndexOf('.') + 1);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// Fallback for unresolved AST nodes
|
||||
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||
return qn.getName().getIdentifier();
|
||||
} else if (mi.getExpression() instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
IMethodBinding mb = mi.resolveMethodBinding();
|
||||
if (mb != null) {
|
||||
ITypeBinding returnType = mb.getReturnType();
|
||||
|
||||
@@ -159,27 +159,52 @@ public class CallGraphBuilder {
|
||||
}
|
||||
String methodName = resolvedValue.substring(lastDot + 1, openParen);
|
||||
|
||||
String declaredType = null;
|
||||
String sourceMethod = null;
|
||||
|
||||
if (varName != null) {
|
||||
// Resolve in the first method in the path where the variable might be declared
|
||||
for (String methodFqn : path) {
|
||||
String declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||
declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||
if (declaredType != null) {
|
||||
sourceMethod = methodFqn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String baseExpr = resolvedValue.substring(0, lastDot);
|
||||
if (baseExpr.contains("new ")) {
|
||||
int newIdx = baseExpr.indexOf("new ");
|
||||
int openParenBase = baseExpr.indexOf('(', newIdx);
|
||||
if (openParenBase > newIdx) {
|
||||
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
|
||||
if (declaredType.contains("<")) {
|
||||
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
|
||||
}
|
||||
sourceMethod = "inline-instantiation";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (declaredType != null) {
|
||||
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType);
|
||||
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
|
||||
List<String> typesToInspect = new ArrayList<>();
|
||||
typesToInspect.add(declaredType);
|
||||
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||
if ("inline-instantiation".equals(sourceMethod)) {
|
||||
typesToInspect.add(declaredType);
|
||||
} else {
|
||||
typesToInspect.add(declaredType);
|
||||
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||
}
|
||||
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
|
||||
|
||||
for (String type : typesToInspect) {
|
||||
Set<String> visited = new HashSet<>();
|
||||
// We must find the compilation unit to pass for simple names!
|
||||
// Let's pass the first available CU for this class
|
||||
TypeDeclaration baseTd = context.getTypeDeclaration(type);
|
||||
CompilationUnit cuToUse = null;
|
||||
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
|
||||
cuToUse = (CompilationUnit) baseTd.getRoot();
|
||||
} else {
|
||||
String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String firstPathMethod = path.get(0);
|
||||
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
|
||||
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
|
||||
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
|
||||
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||
@@ -193,13 +218,54 @@ public class CallGraphBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant
|
||||
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
|
||||
// for string literals or enum-like constants.
|
||||
boolean hasValidConstant = false;
|
||||
for (String ev : polymorphicEvents) {
|
||||
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
|
||||
if (val.equals(val.toUpperCase()) && val.length() > 2) {
|
||||
hasValidConstant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidConstant && resolvedValue.contains("(")) {
|
||||
java.util.List<String> scraped = new java.util.ArrayList<>();
|
||||
|
||||
// Extract "STRING_LITERALS"
|
||||
java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue);
|
||||
while (m1.find()) {
|
||||
scraped.add(m1.group(1));
|
||||
}
|
||||
|
||||
// Extract ENUM_LIKE_CONSTANTS
|
||||
java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue);
|
||||
while (m2.find()) {
|
||||
if (!scraped.contains(m2.group(1))) {
|
||||
scraped.add(m2.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (!scraped.isEmpty()) {
|
||||
polymorphicEvents.clear();
|
||||
polymorphicEvents.addAll(scraped);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones
|
||||
if (polymorphicEvents.size() > 1) {
|
||||
polymorphicEvents.removeIf(e -> {
|
||||
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
|
||||
return !val.equals(val.toUpperCase()) || val.length() <= 2;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
@@ -274,7 +340,11 @@ public class CallGraphBuilder {
|
||||
if (!visited.add(fqn)) return Collections.emptyList();
|
||||
|
||||
List<String> constants = new ArrayList<>();
|
||||
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
||||
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||
if (tempTd == null) {
|
||||
tempTd = context.getTypeDeclaration(className);
|
||||
}
|
||||
final TypeDeclaration td = tempTd;
|
||||
if (td == null) {
|
||||
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
||||
}
|
||||
@@ -322,7 +392,12 @@ public class CallGraphBuilder {
|
||||
} else if (retExpr instanceof QualifiedName qn) {
|
||||
constants.add(qn.toString());
|
||||
} else if (retExpr instanceof SimpleName sn) {
|
||||
constants.add(sn.toString());
|
||||
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
|
||||
if (!consts.isEmpty()) {
|
||||
constants.addAll(consts);
|
||||
} else {
|
||||
constants.add(sn.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,9 +540,23 @@ public class CallGraphBuilder {
|
||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
String exactImpl = null;
|
||||
if (emr.getExpression() instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
String qualifier = getQualifierForVariable(node, varName);
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.BeanDefinition bean = context.getBeanRegistry().resolve(fallbackTypeFqn, varName, qualifier);
|
||||
if (bean != null) {
|
||||
exactImpl = bean.getTypeFqn();
|
||||
}
|
||||
}
|
||||
|
||||
if (exactImpl != null) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(exactImpl + "." + emr.getName().getIdentifier(), args));
|
||||
} else {
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,9 +669,24 @@ public class CallGraphBuilder {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||
|
||||
List<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
Expression receiver = node.getExpression();
|
||||
String exactImpl = null;
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
String qualifier = getQualifierForVariable(node, varName);
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.BeanDefinition bean = context.getBeanRegistry().resolve(className, varName, qualifier);
|
||||
if (bean != null) {
|
||||
exactImpl = bean.getTypeFqn();
|
||||
}
|
||||
}
|
||||
|
||||
if (exactImpl != null) {
|
||||
allResolved.add(exactImpl + "." + methodName);
|
||||
} else {
|
||||
List<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,6 +817,53 @@ public class CallGraphBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getQualifierForVariable(ASTNode contextNode, String varName) {
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(contextNode);
|
||||
if (enclosingMethod != null) {
|
||||
for (Object paramObj : enclosingMethod.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return extractQualifier(svd.modifiers());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TypeDeclaration enclosingType = findEnclosingType(contextNode);
|
||||
if (enclosingType != null) {
|
||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
return extractQualifier(field.modifiers());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractQualifier(List<?> modifiers) {
|
||||
for (Object mod : modifiers) {
|
||||
if (mod instanceof Annotation ann) {
|
||||
String name = ann.getTypeName().getFullyQualifiedName();
|
||||
if (name.endsWith("Qualifier")) {
|
||||
if (ann instanceof SingleMemberAnnotation sma) {
|
||||
return sma.getValue().toString().replace("\"", "");
|
||||
}
|
||||
if (ann instanceof NormalAnnotation na) {
|
||||
for (Object pairObj : na.values()) {
|
||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||
if (pair.getName().getIdentifier().equals("value")) {
|
||||
return pair.getValue().toString().replace("\"", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
||||
@@ -743,9 +894,19 @@ public class CallGraphBuilder {
|
||||
}
|
||||
|
||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||
if (neighbor.equals(target)) return true;
|
||||
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;
|
||||
if (neighbor.endsWith("." + target)) return true;
|
||||
|
||||
boolean neighborHasClass = neighbor.contains(".");
|
||||
boolean targetHasClass = target.contains(".");
|
||||
|
||||
if (neighborHasClass && targetHasClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String simpleTarget = targetHasClass ? target.substring(target.lastIndexOf('.') + 1) : target;
|
||||
String simpleNeighbor = neighborHasClass ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
||||
return simpleNeighbor.equals(simpleTarget);
|
||||
}
|
||||
|
||||
@@ -785,4 +946,113 @@ public class CallGraphBuilder {
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||
List<String> results = new ArrayList<>();
|
||||
|
||||
// 1. Check Field Declarations
|
||||
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
|
||||
Expression right = frag.getInitializer();
|
||||
if (right instanceof MethodInvocation mi) {
|
||||
String calledName = mi.getName().getIdentifier();
|
||||
String targetClass = context.getFqn(td);
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||
} else {
|
||||
String val = constantResolver.resolve(right, context);
|
||||
if (val != null) results.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
Expression left = node.getLeftHandSide();
|
||||
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
||||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
|
||||
|
||||
Expression right = node.getRightHandSide();
|
||||
if (right instanceof MethodInvocation mi) {
|
||||
String calledName = mi.getName().getIdentifier();
|
||||
String targetClass = context.getFqn(td);
|
||||
|
||||
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
|
||||
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
|
||||
} else if (mi.getExpression() instanceof SimpleName) {
|
||||
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
|
||||
if (targetTd != null) {
|
||||
targetClass = context.getFqn(targetTd);
|
||||
}
|
||||
}
|
||||
|
||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||
} else {
|
||||
String val = constantResolver.resolve(right, context);
|
||||
if (val != null) results.add(val);
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(ConstructorInvocation node) {
|
||||
for (Object argObj : node.arguments()) {
|
||||
Expression arg = (Expression) argObj;
|
||||
String val = constantResolver.resolve(arg, context);
|
||||
if (val != null) {
|
||||
if (val.startsWith("ENUM_SET:")) {
|
||||
for (String eVal : val.substring(9).split(",")) {
|
||||
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||
}
|
||||
} else {
|
||||
results.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperConstructorInvocation node) {
|
||||
for (Object argObj : node.arguments()) {
|
||||
Expression arg = (Expression) argObj;
|
||||
String val = constantResolver.resolve(arg, context);
|
||||
if (val != null) {
|
||||
if (val.startsWith("ENUM_SET:")) {
|
||||
for (String eVal : val.substring(9).split(",")) {
|
||||
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||
}
|
||||
} else {
|
||||
results.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Check Constructors
|
||||
for (MethodDeclaration md : td.getMethods()) {
|
||||
if (md.isConstructor() && md.getBody() != null) {
|
||||
md.getBody().accept(assignmentVisitor);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check Initializers
|
||||
for (Object bodyDecl : td.bodyDeclarations()) {
|
||||
if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) {
|
||||
init.getBody().accept(assignmentVisitor);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.eclipse.jdt.core.JavaCore;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BeanRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BeanScanner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -31,6 +33,7 @@ public class CodebaseContext {
|
||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||
private final PropertyResolver propertyResolver = new PropertyResolver();
|
||||
private final BeanRegistry beanRegistry = new BeanRegistry(this);
|
||||
private final List<String> activeProfiles = new ArrayList<>();
|
||||
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
||||
private List<LibraryHint> libraryHints = new ArrayList<>();
|
||||
@@ -167,6 +170,10 @@ public class CodebaseContext {
|
||||
scan(Set.of(rootDir), Collections.emptySet());
|
||||
}
|
||||
|
||||
public BeanRegistry getBeanRegistry() {
|
||||
return beanRegistry;
|
||||
}
|
||||
|
||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||
|
||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||
@@ -194,6 +201,11 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-processing: scan all parsed CompilationUnits for Spring beans
|
||||
for (CompilationUnit cu : allCus) {
|
||||
BeanScanner.scanAndRegister(cu, this, beanRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
||||
@@ -615,14 +627,31 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly
|
||||
if (!isAbstract && extendsStateMachineConfigurerAdapter(td)) {
|
||||
// If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly or has configurer methods
|
||||
if (!isAbstract && (extendsStateMachineConfigurerAdapter(td) || hasConfigurerMethod(td))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasConfigurerMethod(TypeDeclaration td) {
|
||||
for (MethodDeclaration method : td.getMethods()) {
|
||||
if (method.parameters().isEmpty()) continue;
|
||||
for (Object paramObj : method.parameters()) {
|
||||
if (paramObj instanceof SingleVariableDeclaration param) {
|
||||
String paramType = param.getType().toString();
|
||||
if (paramType.contains("StateMachineTransitionConfigurer") ||
|
||||
paramType.contains("StateMachineStateConfigurer") ||
|
||||
paramType.contains("StateMachineConfigurationConfigurer")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
||||
}
|
||||
|
||||
@@ -27,13 +27,18 @@ public class GoldenUpdater {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
List<TestScenario> scenarios = provideTestScenarios();
|
||||
Path rootDir = Path.of("state_machine_exporter");
|
||||
Path projectRoot = Path.of(System.getProperty("user.dir"));
|
||||
if (projectRoot.endsWith("state_machine_exporter")) {
|
||||
projectRoot = projectRoot.getParent();
|
||||
}
|
||||
Path rootDir = projectRoot.resolve("state_machine_exporter");
|
||||
|
||||
for (TestScenario scenario : scenarios) {
|
||||
System.out.println("Updating golden files for: " + scenario.name());
|
||||
Path tempDir = Files.createTempDirectory("golden-update-");
|
||||
try {
|
||||
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true);
|
||||
Path inputPath = projectRoot.resolve(scenario.inputPath());
|
||||
exportService.runExporter(inputPath, tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles());
|
||||
|
||||
List<Path> generatedDirs;
|
||||
try (var stream = Files.list(tempDir)) {
|
||||
@@ -158,6 +163,49 @@ public class GoldenUpdater {
|
||||
Path.of("state_machines/inheritance_extra_functions3_state_machine"),
|
||||
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
||||
"G2StateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"Extended Analysis Sample",
|
||||
Path.of("state_machines/extended_analysis_sample"),
|
||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
||||
"ExtendedStateMachineConfig"
|
||||
),
|
||||
new TestScenario(
|
||||
"Extended Analysis Sample (PROD)",
|
||||
Path.of("state_machines/extended_analysis_sample"),
|
||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"),
|
||||
"ExtendedStateMachineConfig",
|
||||
List.of("prod")
|
||||
),
|
||||
new TestScenario(
|
||||
"Inheritance Sample",
|
||||
Path.of("state_machines/inheritance_sample"),
|
||||
Path.of("src/test/resources/golden/InheritanceStateMachineConfig"),
|
||||
"InheritanceStateMachineConfig"
|
||||
),
|
||||
new TestScenario(
|
||||
"Enterprise Order System",
|
||||
Path.of("state_machines/enterprise_order_system"),
|
||||
Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
|
||||
"EnterpriseStateMachineConfig"
|
||||
),
|
||||
new TestScenario(
|
||||
"Multi-Module Sample",
|
||||
Path.of("state_machines/multi_module_sample/core-module"),
|
||||
Path.of("src/test/resources/golden/OrderStateMachineConfig"),
|
||||
"OrderStateMachineConfig"
|
||||
),
|
||||
new TestScenario(
|
||||
"Maven Multi-Module Sample",
|
||||
Path.of("state_machines/maven_multi_module/core-module"),
|
||||
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
||||
"MavenOrderStateMachine"
|
||||
),
|
||||
new TestScenario(
|
||||
"Complex Multi-Module Sample",
|
||||
Path.of("state_machines/complex_multi_module_sm"),
|
||||
Path.of("src/test/resources/golden/StateMachineConfig"),
|
||||
"StateMachineConfig"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,4 +192,190 @@ class TransitionLinkerEnricherTest {
|
||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFalsePositiveOnSubstringContains() {
|
||||
// This test proves that the heuristic is entirely removed and false positives are eliminated
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||
t1.setEvent(Event.of("PAY", "PAY"));
|
||||
|
||||
// Trigger point sends "PAYMENT" which contains "PAY". Under the old heuristics, this would falsely match!
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("PAYMENT").build())
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(result, null, null);
|
||||
|
||||
// It should NOT match, because "PAYMENT" != "PAY" strictly.
|
||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldFilterCrossStateMachineRoutingByVertical() {
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
|
||||
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||
.methodChain(List.of(
|
||||
"com.example.electronics.ElectronicsOrderController.post()",
|
||||
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"
|
||||
))
|
||||
.build();
|
||||
|
||||
// Testing against Electronics SM - should match
|
||||
AnalysisResult resultElectronics = AnalysisResult.builder()
|
||||
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(resultElectronics, null, null);
|
||||
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
|
||||
|
||||
// Testing against ComputerStore SM - should NOT match because chain has Electronics service
|
||||
AnalysisResult resultComputer = AnalysisResult.builder()
|
||||
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration") // the non-electronics one
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(resultComputer, null, null);
|
||||
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
|
||||
assertThat(updatedChainComp.getMatchedTransitions()).isNullOrEmpty();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldAllowSharedServicesToRouteToMultipleStateMachines() {
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
|
||||
t1.setEvent(Event.of("PROCESS", "PROCESS"));
|
||||
|
||||
// Chain contains NO vertical prefix (e.g. CommonOrderService)
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("PROCESS").build())
|
||||
.methodChain(List.of(
|
||||
"com.example.CommonOrderController.post()",
|
||||
"com.example.CommonOrderService.processOrderEvent()"
|
||||
))
|
||||
.build();
|
||||
|
||||
AnalysisResult resultElectronics = AnalysisResult.builder()
|
||||
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(resultElectronics, null, null);
|
||||
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
|
||||
|
||||
// Should match Electronics because it's a shared service (no computerstore service in path)
|
||||
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
|
||||
|
||||
AnalysisResult resultComputer = AnalysisResult.builder()
|
||||
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(resultComputer, null, null);
|
||||
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
|
||||
|
||||
// Should ALSO match ComputerStore because it's a shared service
|
||||
assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldFilterCrossStateMachineRoutingWithMultipleVerticals() {
|
||||
Transition t1 = new Transition();
|
||||
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
|
||||
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
|
||||
|
||||
// Chain 1: Electronics
|
||||
CallChain chainElec = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||
.methodChain(List.of("com.example.electronics.ElectronicsOrderController.post()"))
|
||||
.build();
|
||||
|
||||
// Chain 2: Furniture
|
||||
CallChain chainFurn = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||
.methodChain(List.of("com.example.furniture.FurnitureOrderController.post()"))
|
||||
.build();
|
||||
|
||||
// Chain 3: Groceries
|
||||
CallChain chainGroc = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||
.methodChain(List.of("com.example.groceries.GroceriesOrderController.post()"))
|
||||
.build();
|
||||
|
||||
// SM 1: Electronics
|
||||
AnalysisResult resElec = AnalysisResult.builder()
|
||||
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||
.build();
|
||||
|
||||
// SM 2: Furniture
|
||||
AnalysisResult resFurn = AnalysisResult.builder()
|
||||
.name("com.example.furniture.FurnitureOwnDeliveryStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||
.build();
|
||||
|
||||
// SM 3: Groceries
|
||||
AnalysisResult resGroc = AnalysisResult.builder()
|
||||
.name("com.example.groceries.GroceriesStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||
.build();
|
||||
|
||||
// SM 4: Computer Store (The catch-all base one in computerstore package)
|
||||
AnalysisResult resComp = AnalysisResult.builder()
|
||||
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
|
||||
.transitions(List.of(t1))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||
.build();
|
||||
|
||||
// Test Electronics SM - Should only keep Electronics Chain
|
||||
enricher.enrich(resElec, null, null);
|
||||
assertThat(resElec.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1); // Elec
|
||||
assertThat(resElec.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||
assertThat(resElec.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||
|
||||
// Test Furniture SM - Should only keep Furniture Chain
|
||||
enricher.enrich(resFurn, null, null);
|
||||
assertThat(resFurn.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||
assertThat(resFurn.getMetadata().getCallChains().get(1).getMatchedTransitions()).hasSize(1); // Furn
|
||||
assertThat(resFurn.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||
|
||||
// Test Groceries SM - Should only keep Groceries Chain
|
||||
enricher.enrich(resGroc, null, null);
|
||||
assertThat(resGroc.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||
assertThat(resGroc.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||
assertThat(resGroc.getMetadata().getCallChains().get(2).getMatchedTransitions()).hasSize(1); // Groc
|
||||
|
||||
// Test ComputerStore SM - Should reject ALL because none belong to computerstore
|
||||
enricher.enrich(resComp, null, null);
|
||||
assertThat(resComp.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||
assertThat(resComp.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||
assertThat(resComp.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class BeanRegistryTest {
|
||||
|
||||
private TestCodebaseContext context;
|
||||
private BeanRegistry registry;
|
||||
|
||||
static class TestCodebaseContext extends CodebaseContext {
|
||||
private final Map<String, List<String>> impls = new HashMap<>();
|
||||
|
||||
public void setImpls(String fqn, List<String> implList) {
|
||||
impls.put(fqn, implList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getImplementations(String fqn) {
|
||||
return impls.getOrDefault(fqn, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
context = new TestCodebaseContext();
|
||||
registry = new BeanRegistry(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldMatchByExactType() {
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("paymentService")
|
||||
.typeFqn("com.example.PaymentService")
|
||||
.qualifiers(List.of())
|
||||
.isPrimary(false)
|
||||
.build());
|
||||
|
||||
BeanDefinition resolved = registry.resolve("com.example.PaymentService", null, null);
|
||||
assertThat(resolved).isNotNull();
|
||||
assertThat(resolved.getName()).isEqualTo("paymentService");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldMatchByQualifierBypassingNameAndType() {
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("emailNotifier")
|
||||
.typeFqn("com.example.EmailNotifier")
|
||||
.qualifiers(List.of("email"))
|
||||
.isPrimary(false)
|
||||
.build());
|
||||
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("smsNotifier")
|
||||
.typeFqn("com.example.SmsNotifier")
|
||||
.qualifiers(List.of("sms"))
|
||||
.isPrimary(false)
|
||||
.build());
|
||||
|
||||
context.setImpls("com.example.Notifier", List.of("com.example.EmailNotifier", "com.example.SmsNotifier"));
|
||||
|
||||
BeanDefinition resolved = registry.resolve("com.example.Notifier", "emailNotifier", "sms");
|
||||
|
||||
assertThat(resolved).isNotNull();
|
||||
assertThat(resolved.getName()).isEqualTo("smsNotifier");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldMatchByNameWhenNoQualifier() {
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("fastProcessor")
|
||||
.typeFqn("com.example.FastProcessor")
|
||||
.qualifiers(List.of())
|
||||
.isPrimary(false)
|
||||
.build());
|
||||
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("slowProcessor")
|
||||
.typeFqn("com.example.SlowProcessor")
|
||||
.qualifiers(List.of())
|
||||
.isPrimary(false)
|
||||
.build());
|
||||
|
||||
context.setImpls("com.example.Processor", List.of("com.example.FastProcessor", "com.example.SlowProcessor"));
|
||||
|
||||
BeanDefinition resolved = registry.resolve("com.example.Processor", "fastProcessor", null);
|
||||
|
||||
assertThat(resolved).isNotNull();
|
||||
assertThat(resolved.getName()).isEqualTo("fastProcessor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldMatchByPrimary() {
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("localStorage")
|
||||
.typeFqn("com.example.LocalStorage")
|
||||
.qualifiers(List.of())
|
||||
.isPrimary(false)
|
||||
.build());
|
||||
|
||||
registry.register(BeanDefinition.builder()
|
||||
.name("cloudStorage")
|
||||
.typeFqn("com.example.CloudStorage")
|
||||
.qualifiers(List.of())
|
||||
.isPrimary(true)
|
||||
.build());
|
||||
|
||||
context.setImpls("com.example.Storage", List.of("com.example.LocalStorage", "com.example.CloudStorage"));
|
||||
|
||||
// Name doesn't match either, no qualifier
|
||||
BeanDefinition resolved = registry.resolve("com.example.Storage", "storage", null);
|
||||
|
||||
assertThat(resolved).isNotNull();
|
||||
assertThat(resolved.getName()).isEqualTo("cloudStorage");
|
||||
}
|
||||
}
|
||||
@@ -1081,4 +1081,888 @@ class CallGraphBuilderTest {
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGracefullyHandleComplexMethodChainsWithoutCrashing() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.Optional;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(DomainEvent domainEvent) {
|
||||
service.process(Optional.of(domainEvent).get().getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public String getEvent() { return "COMPLEX_EVENT"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_complex_chain");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCrashOnStaticMethodWrapping() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(DomainEvent domainEvent) {
|
||||
service.process(String.valueOf(domainEvent.getEvent()));
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public String getEvent() { return "STATIC_WRAP_EVENT"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_static_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("handleStatus").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromStringConcatenation() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(DomainEvent domainEvent) {
|
||||
service.process(domainEvent.getCustomEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public static final String PREFIX = "ORDER_";
|
||||
public String getCustomEvent() {
|
||||
return PREFIX + "CREATED";
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_concat");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_CREATED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromEnumReturn() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(DomainEvent domainEvent) {
|
||||
service.process(domainEvent.getEventEnum());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public String getEventEnum() {
|
||||
return MyEvents.ORDER_PAID.name();
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
|
||||
enum MyEvents { ORDER_PAID }
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_enum_return");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_PAID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromAlternativeGetterName() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(DomainEvent domainEvent) {
|
||||
service.process(domainEvent.get2Type());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public String get2Type() {
|
||||
return "ALTERNATIVE_EVENT";
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_alt_getter");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromStringToString() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(DomainEvent domainEvent) {
|
||||
service.process(domainEvent.getEventString());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public static final String ACTUAL_STRING = "STRING_LITERAL_EVENT";
|
||||
public String getEventString() {
|
||||
return ACTUAL_STRING.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_string_tostring");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("STRING_LITERAL_EVENT");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromInlineInstantiation() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new DomainEvent().getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
public String getEvent() { return "INLINE_EVENT"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new DomainEvent("PAYLOAD_ID").getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
private String id;
|
||||
public DomainEvent(String id) { this.id = id; }
|
||||
public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new DomainEvent<String>().getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEvent<T> {
|
||||
public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new SpecificDomainEvent().getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseDomainEvent {
|
||||
public String getEvent() { return "BASE_EVENT_RETURN"; }
|
||||
}
|
||||
|
||||
class SpecificDomainEvent extends BaseDomainEvent {
|
||||
// no getEvent here, it inherits it!
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus(Order o, String abc) {
|
||||
service.process(new DomainEvent(o.getId(), abc).getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
public String getId() { return "123"; }
|
||||
}
|
||||
|
||||
class DomainEvent {
|
||||
private String id;
|
||||
private String extra;
|
||||
public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
|
||||
public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new BaseEvent().getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
class BaseEvent {
|
||||
public String getEvent() { return "BASE_EVENT_RETURN"; }
|
||||
}
|
||||
|
||||
class SubEvent extends BaseEvent {
|
||||
public String getEvent() { return "SUB_EVENT_RETURN"; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
// Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new OrderPickedUpEvent().getType());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderPickedUpEvent {
|
||||
private String type;
|
||||
public OrderPickedUpEvent() {
|
||||
this.type = "PICKED_UP_IN_CONSTRUCTOR";
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_field_const");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("PICKED_UP_IN_CONSTRUCTOR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new OrderCancelledEvent().getType());
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseEvent {
|
||||
private String type;
|
||||
public BaseEvent(String type, String otherVar) {
|
||||
this.type = type;
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderCancelledEvent extends BaseEvent {
|
||||
public OrderCancelledEvent() {
|
||||
super("CANCELLED_IN_SUPER", "someVar");
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_super_const");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_IN_SUPER");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromFieldAssignedToAnotherConstantInConstructor() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new EventWithIndirectConstant().getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventConstants {
|
||||
public static final String INDIRECT_EVENT = "INDIRECT_EVENT_VALUE";
|
||||
}
|
||||
|
||||
class EventWithIndirectConstant {
|
||||
private String type;
|
||||
public EventWithIndirectConstant() {
|
||||
this.type = EventConstants.INDIRECT_EVENT;
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_indirect_const");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("INDIRECT_EVENT_VALUE");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromConstructorInvocation() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new EventWithThisCall().getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventWithThisCall {
|
||||
private String type;
|
||||
public EventWithThisCall() {
|
||||
this("THIS_CALL_CONSTANT");
|
||||
}
|
||||
public EventWithThisCall(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_this_const");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new OrderCancelledEvent().getType());
|
||||
}
|
||||
}
|
||||
|
||||
class OrderCancelledEvent {
|
||||
private String cancelEventType;
|
||||
public OrderCancelledEvent() {
|
||||
this.cancelEventType = assertMatchingCancelEventType("some_sender");
|
||||
}
|
||||
private String assertMatchingCancelEventType(String sender) {
|
||||
return "CANCELLED_BY_SENDER";
|
||||
}
|
||||
public String getType() { return cancelEventType; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromAbstractSuperclass() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
service.process(new PostAcceptanceOrderModificationsRejectedEvent().getType());
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractPostAcceptanceOrderModificationsEvent {
|
||||
private String event;
|
||||
public AbstractPostAcceptanceOrderModificationsEvent(String event) {
|
||||
this.event = event;
|
||||
}
|
||||
public String getType() { return event; }
|
||||
}
|
||||
|
||||
class PostAcceptanceOrderModificationsRejectedEvent extends AbstractPostAcceptanceOrderModificationsEvent {
|
||||
public PostAcceptanceOrderModificationsRejectedEvent() {
|
||||
super("POST_ACCEPTANCE_REJECTED");
|
||||
}
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_post_acc");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus1() {
|
||||
service.process(new EventWithFieldDecl().getType());
|
||||
}
|
||||
public void handleStatus2() {
|
||||
service.process(new EventWithInitBlock().getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventWithFieldDecl {
|
||||
private String event = "FIELD_DECL";
|
||||
public String getType() { return event; }
|
||||
}
|
||||
|
||||
class EventWithInitBlock {
|
||||
private String event;
|
||||
{
|
||||
event = "INIT_BLOCK";
|
||||
}
|
||||
public String getType() { return event; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_field_init");
|
||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
CallGraphBuilder builder = new CallGraphBuilder(context);
|
||||
|
||||
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
|
||||
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
|
||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||
List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
|
||||
|
||||
java.util.List<String> allEvents = new java.util.ArrayList<>();
|
||||
chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
|
||||
org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void handleStatus() {
|
||||
// The AST tracer will trace EventWithConstructorArg.getType()
|
||||
// It will find `this.type = typeArg`, which it cannot resolve because it's a parameter.
|
||||
// The fallback scraper should catch "COMPLEX_INLINE_CONST".
|
||||
service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType());
|
||||
}
|
||||
}
|
||||
|
||||
class EventWithConstructorArg {
|
||||
private String type;
|
||||
public EventWithConstructorArg(String typeArg, int other) {
|
||||
this.type = typeArg;
|
||||
}
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
public void process(String event) {}
|
||||
}
|
||||
""";
|
||||
Path tempDir = Files.createTempDirectory("callgraph_scraper");
|
||||
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<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNarrowPolymorphicCallUsingBeanResolution(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
public interface Notifier {
|
||||
void send(String event);
|
||||
}
|
||||
|
||||
@Component("emailNotifier")
|
||||
class EmailNotifier implements Notifier {
|
||||
public void send(String event) {
|
||||
// Email implementation
|
||||
}
|
||||
}
|
||||
|
||||
@Component("smsNotifier")
|
||||
class SmsNotifier implements Notifier {
|
||||
public void send(String event) {
|
||||
// SMS implementation
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class OrderService {
|
||||
private final Notifier smsNotifier;
|
||||
|
||||
public OrderService(Notifier smsNotifier) {
|
||||
this.smsNotifier = smsNotifier;
|
||||
}
|
||||
|
||||
public void processOrder() {
|
||||
smsNotifier.send("ORDER_COMPLETED");
|
||||
}
|
||||
}
|
||||
""";
|
||||
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 triggerEmail = TriggerPoint.builder()
|
||||
.className("com.example.EmailNotifier")
|
||||
.methodName("send")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
TriggerPoint triggerSms = TriggerPoint.builder()
|
||||
.className("com.example.SmsNotifier")
|
||||
.methodName("send")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(triggerEmail, triggerSms));
|
||||
|
||||
// Because 'smsNotifier' perfectly matches the component name "smsNotifier",
|
||||
// the bean resolution should narrow the call graph to ONLY SmsNotifier.
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getClassName()).isEqualTo("com.example.SmsNotifier");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Reference in New Issue
Block a user