Compare commits
1 Commits
ai-branch-
...
ff16c1bbe7
| Author | SHA1 | Date | |
|---|---|---|---|
| ff16c1bbe7 |
@@ -239,12 +239,20 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
return null;
|
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) {
|
private String getDeepSharedPackage(String pkg1, String pkg2) {
|
||||||
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
|
||||||
String[] p1 = pkg1.split("\\.");
|
|
||||||
|
String domain1 = getDomainRootPackage(pkg1.split("\\."));
|
||||||
|
String[] p1 = domain1.split("\\.");
|
||||||
String[] p2 = pkg2.split("\\.");
|
String[] p2 = pkg2.split("\\.");
|
||||||
|
|
||||||
// Find the deepest common segment index
|
|
||||||
int matchIdx = -1;
|
int matchIdx = -1;
|
||||||
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
if (p1[i].equals(p2[i])) {
|
if (p1[i].equals(p2[i])) {
|
||||||
@@ -253,8 +261,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If they share at least 3 segments (e.g. com.company.electronics), it's a deep match
|
|
||||||
if (matchIdx >= 2) {
|
// Require matching up to the domain root
|
||||||
|
int requiredMatchIdx = Math.max(1, p1.length - 1);
|
||||||
|
if (matchIdx >= requiredMatchIdx) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for(int i = 0; i <= matchIdx; i++) {
|
for(int i = 0; i <= matchIdx; i++) {
|
||||||
sb.append(p1[i]).append(".");
|
sb.append(p1[i]).append(".");
|
||||||
@@ -266,13 +276,24 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
|
|
||||||
private boolean isDeepPackageDivergence(String pkg1, String pkg2) {
|
private boolean isDeepPackageDivergence(String pkg1, String pkg2) {
|
||||||
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false;
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false;
|
||||||
String[] p1 = pkg1.split("\\.");
|
|
||||||
|
String domain1 = getDomainRootPackage(pkg1.split("\\."));
|
||||||
|
String[] p1 = domain1.split("\\.");
|
||||||
String[] p2 = pkg2.split("\\.");
|
String[] p2 = pkg2.split("\\.");
|
||||||
|
|
||||||
// If they share exactly the root (com.company) but diverge at the 3rd segment (e.g. electronics vs computerstore)
|
int matchIdx = -1;
|
||||||
// Then it's a divergence.
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
if (p1.length >= 3 && p2.length >= 3) {
|
if (p1[i].equals(p2[i])) {
|
||||||
return p1[0].equals(p2[0]) && p1[1].equals(p2[1]) && !p1[2].equals(p2[2]);
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -540,9 +540,23 @@ public class CallGraphBuilder {
|
|||||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
String exactImpl = null;
|
||||||
for (String impl : impls) {
|
if (emr.getExpression() instanceof SimpleName sn) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,9 +669,24 @@ public class CallGraphBuilder {
|
|||||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(className);
|
Expression receiver = node.getExpression();
|
||||||
for (String impl : impls) {
|
String exactImpl = null;
|
||||||
allResolved.add(impl + "." + methodName);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,6 +817,53 @@ public class CallGraphBuilder {
|
|||||||
return null;
|
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) {
|
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 (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||||
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
||||||
@@ -818,9 +894,19 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (neighbor.equals(target)) return true;
|
||||||
if (target.endsWith("." + neighbor)) return true;
|
if (target.endsWith("." + neighbor)) return true;
|
||||||
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
if (neighbor.endsWith("." + target)) return true;
|
||||||
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
|
||||||
|
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);
|
return simpleNeighbor.equals(simpleTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.eclipse.jdt.core.JavaCore;
|
import org.eclipse.jdt.core.JavaCore;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
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.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -31,6 +33,7 @@ public class CodebaseContext {
|
|||||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
private final PropertyResolver propertyResolver = new PropertyResolver();
|
private final PropertyResolver propertyResolver = new PropertyResolver();
|
||||||
|
private final BeanRegistry beanRegistry = new BeanRegistry(this);
|
||||||
private final List<String> activeProfiles = new ArrayList<>();
|
private final List<String> activeProfiles = new ArrayList<>();
|
||||||
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
||||||
private List<LibraryHint> libraryHints = new ArrayList<>();
|
private List<LibraryHint> libraryHints = new ArrayList<>();
|
||||||
@@ -167,6 +170,10 @@ public class CodebaseContext {
|
|||||||
scan(Set.of(rootDir), Collections.emptySet());
|
scan(Set.of(rootDir), Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BeanRegistry getBeanRegistry() {
|
||||||
|
return beanRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||||
|
|
||||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
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) {
|
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 it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly or has configurer methods
|
||||||
if (!isAbstract && extendsStateMachineConfigurerAdapter(td)) {
|
if (!isAbstract && (extendsStateMachineConfigurerAdapter(td) || hasConfigurerMethod(td))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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) {
|
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||||
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,18 @@ public class GoldenUpdater {
|
|||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
List<TestScenario> scenarios = provideTestScenarios();
|
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) {
|
for (TestScenario scenario : scenarios) {
|
||||||
System.out.println("Updating golden files for: " + scenario.name());
|
System.out.println("Updating golden files for: " + scenario.name());
|
||||||
Path tempDir = Files.createTempDirectory("golden-update-");
|
Path tempDir = Files.createTempDirectory("golden-update-");
|
||||||
try {
|
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;
|
List<Path> generatedDirs;
|
||||||
try (var stream = Files.list(tempDir)) {
|
try (var stream = Files.list(tempDir)) {
|
||||||
@@ -158,6 +163,49 @@ public class GoldenUpdater {
|
|||||||
Path.of("state_machines/inheritance_extra_functions3_state_machine"),
|
Path.of("state_machines/inheritance_extra_functions3_state_machine"),
|
||||||
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
||||||
"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"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1082,7 +1082,6 @@ class CallGraphBuilderTest {
|
|||||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
|
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
|
||||||
String source = """
|
String source = """
|
||||||
@@ -1897,4 +1896,73 @@ class CallGraphBuilderTest {
|
|||||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
|
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