multi pom

This commit is contained in:
2026-06-17 07:11:57 +02:00
parent 1f4a1667c3
commit f48cdf080a
59 changed files with 3622 additions and 296 deletions

View File

@@ -87,9 +87,11 @@ public class AnalysisResult {
ep.setName(resolver.resolveValue(ep.getName(), properties));
}
if (ep.getMetadata() != null) {
Map<String, String> resolvedMeta = new java.util.HashMap<>();
for (var entry : ep.getMetadata().entrySet()) {
entry.setValue(resolver.resolveValue(entry.getValue(), properties));
resolvedMeta.put(entry.getKey(), resolver.resolveValue(entry.getValue(), properties));
}
ep.setMetadata(resolvedMeta);
}
}
}

View File

@@ -29,6 +29,7 @@ public class EntryPoint {
private String name; // e.g., "POST /orders" or "Kafka Topic: orders"
private final String className;
private final String methodName;
private final String sourceFile;
private Map<String, String> metadata; // e.g., path, topic, exchange
@Builder.Default
private final List<Parameter> parameters = java.util.Collections.emptyList();

View File

@@ -15,6 +15,7 @@ public class TriggerPoint {
private String event;
private final String className;
private final String methodName;
private final String sourceFile;
private final String sourceModule;
private final String stateMachineId; // Optional: to link to a specific SM instance
private final int lineNumber;

View File

@@ -82,40 +82,49 @@ public class SiblingDependencyResolver {
Set<Path> siblings = new HashSet<>();
Path pom = projectDir.resolve("pom.xml");
if (Files.exists(pom)) {
log.info("Found pom.xml at {}", pom);
String content = Files.readString(pom);
Matcher matcher = MAVEN_DEPENDENCY.matcher(content);
Set<String> targetArtifacts = new HashSet<>();
while (matcher.find()) {
targetArtifacts.add(matcher.group(2)); // Just tracking artifactId for simplicity
Matcher matcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(content);
// We want artifacts from the <dependencies> section
int depsStart = content.indexOf("<dependencies>");
int depsEnd = content.indexOf("</dependencies>");
if (depsStart != -1 && depsEnd > depsStart) {
String depsContent = content.substring(depsStart, depsEnd);
Matcher depMatcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(depsContent);
while (depMatcher.find()) {
targetArtifacts.add(depMatcher.group(1));
}
}
log.info("Found maven target artifacts: {}", targetArtifacts);
if (!targetArtifacts.isEmpty()) {
Path rootDir = findMavenRoot(projectDir);
log.info("Resolved Maven root to: {}", rootDir);
if (rootDir != null) {
// Find all pom.xml files in the workspace (excluding node_modules, build, etc.)
try (Stream<Path> paths = Files.walk(rootDir)) {
paths.filter(p -> p.getFileName().toString().equals("pom.xml")
&& !p.toString().contains("/target/")
&& !p.toString().contains("/node_modules/"))
.forEach(p -> {
try {
String pContent = Files.readString(p);
Matcher artMatcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(pContent);
if (artMatcher.find()) {
String artifactId = artMatcher.group(1);
if (targetArtifacts.contains(artifactId)) {
Path siblingDir = p.getParent();
// Don't add ourselves
if (!siblingDir.equals(projectDir)) {
log.info("Discovered local Maven sibling dependency: {}", siblingDir);
siblings.add(siblingDir);
}
}
List<Path> allPoms = paths.filter(p -> p.getFileName().toString().equals("pom.xml"))
.filter(p -> !p.toString().contains("/target/"))
.collect(Collectors.toList());
log.info("Scanned {} total pom.xml files under {}", allPoms.size(), rootDir);
for (Path p : allPoms) {
try {
String pContent = Files.readString(p);
String artifactId = extractOwnArtifactId(pContent);
log.info("Checking artifactId {} in {}", artifactId, p);
if (artifactId != null && targetArtifacts.contains(artifactId)) {
Path siblingDir = p.getParent();
if (!siblingDir.equals(projectDir)) {
log.info("Discovered local Maven sibling dependency: {}", siblingDir);
siblings.add(siblingDir);
}
} catch (IOException e) {
// ignore
}
});
} catch (IOException e) {
// ignore
}
}
}
}
}
@@ -123,6 +132,21 @@ public class SiblingDependencyResolver {
return siblings;
}
private String extractOwnArtifactId(String pomContent) {
// Strip <parent> block if it exists
String content = pomContent;
int parentEnd = pomContent.indexOf("</parent>");
if (parentEnd != -1) {
content = pomContent.substring(parentEnd);
}
Matcher matcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private Path findMavenRoot(Path start) {
Path current = start.toAbsolutePath();
Path lastPomDir = null;

View File

@@ -125,6 +125,7 @@ public class GenericEventDetector {
.event(eventValue)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.build();
}

View File

@@ -26,12 +26,52 @@ public class SpringMvcDetector {
if (isRestController(node)) {
processController(node, entryPoints);
}
// Support WebFlux RouterFunction beans
processRouterFunctions(node, entryPoints);
return super.visit(node);
}
});
return entryPoints;
}
private void processRouterFunctions(TypeDeclaration td, List<EntryPoint> entryPoints) {
for (MethodDeclaration method : td.getMethods()) {
if (isBeanMethod(method) && method.getReturnType2() != null && method.getReturnType2().toString().contains("RouterFunction")) {
method.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation call) {
String name = call.getName().getIdentifier();
String verb = getHttpVerb(name);
if (verb != null && !call.arguments().isEmpty()) {
Expression pathArg = (Expression) call.arguments().get(0);
String path = constantResolver.resolve(pathArg, context);
if (path == null) path = pathArg.toString().replaceAll("\"", "");
entryPoints.add(EntryPoint.builder()
.type(EntryPoint.Type.REST)
.name(verb + " " + path)
.className(context.getFqn(td))
.methodName(method.getName().getIdentifier())
.sourceFile(context.getRelativePath(context.getFqn(td)))
.metadata(Map.of("path", path, "verb", verb, "style", "WebFlux Functional"))
.build());
}
return super.visit(call);
}
});
}
}
}
private boolean isBeanMethod(MethodDeclaration method) {
for (Object mod : method.modifiers()) {
if (mod instanceof Annotation ann && ann.getTypeName().getFullyQualifiedName().endsWith("Bean")) {
return true;
}
}
return false;
}
private boolean isRestController(TypeDeclaration node) {
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation annotation) {
@@ -123,6 +163,7 @@ public class SpringMvcDetector {
.name(verb + " " + fullPath)
.className(context.getFqn(controller))
.methodName(method.getName().getIdentifier())
.sourceFile(context.getRelativePath(context.getFqn(controller)))
.metadata(metadata)
.parameters(parameters)
.build();

View File

@@ -7,19 +7,7 @@ import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodReference;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -290,11 +278,30 @@ public class AstTransitionParser {
for (MethodDeclaration md : td.getMethods()) {
if (md.getName().getIdentifier().equals(binding.getName()) &&
md.parameters().size() == binding.getParameterTypes().length) {
// Special case: if it's a getter/factory for Action/Guard, look into the return statement
if (md.getBody() != null) {
if (md.getBody().statements().size() == 1) {
Object stmt = md.getBody().statements().get(0);
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
Expression retExpr = rs.getExpression();
if (isLambdaOrAnonymous(retExpr)) {
return retExpr.toString();
}
}
}
}
return md.toString();
}
}
}
}
} else {
// FALLBACK: Manual resolution by name if binding failed
TypeDeclaration currentClass = findEnclosingType(mi);
if (currentClass != null) {
return resolveMethodManually(mi.getName().getIdentifier(), currentClass, context);
}
}
}
@@ -322,12 +329,55 @@ public class AstTransitionParser {
return null;
}
private static String resolveMethodManually(String methodName, TypeDeclaration td, CodebaseContext context) {
// Search in this class
for (MethodDeclaration md : td.getMethods()) {
if (md.getName().getIdentifier().equals(methodName)) {
// Found it. Apply the return-statement-extraction logic here too
if (md.getBody() != null && md.getBody().statements().size() == 1) {
Object stmt = md.getBody().statements().get(0);
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
Expression retExpr = rs.getExpression();
if (isLambdaOrAnonymous(retExpr)) {
return retExpr.toString();
}
}
}
return md.toString();
}
}
// Search in superclass
if (td.getSuperclassType() != null) {
String superName = td.getSuperclassType().toString();
// Try to resolve simple name to FQN if possible, or just search by simple name
TypeDeclaration superTd = context.getTypeDeclaration(superName);
if (superTd != null) {
return resolveMethodManually(methodName, superTd, context);
}
}
return null;
}
private static TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof TypeDeclaration td) return td;
current = current.getParent();
}
return null;
}
private static void parseGuard(Object arg, Transition t, CompilationUnit cu, CodebaseContext context) {
QuotedExpression quotedExpr = QuotedExpression.of(arg);
if (quotedExpr != null) {
String internalLogic = extractInternalLogic(quotedExpr.getExpression(), cu, context);
int lineNumber = cu.getLineNumber(quotedExpr.getExpression().getStartPosition());
t.setGuard(Guard.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber));
TypeDeclaration td = findEnclosingType(quotedExpr.getExpression());
String fqn = td != null ? context.getFqn(td) : null;
String sourceFile = fqn != null ? context.getRelativePath(fqn) : null;
t.setGuard(Guard.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber, fqn, sourceFile));
}
}
@@ -336,7 +386,10 @@ public class AstTransitionParser {
if (quotedExpr != null) {
String internalLogic = extractInternalLogic(quotedExpr.getExpression(), cu, context);
int lineNumber = cu.getLineNumber(quotedExpr.getExpression().getStartPosition());
t.getActions().add(Action.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber));
TypeDeclaration td = findEnclosingType(quotedExpr.getExpression());
String fqn = td != null ? context.getFqn(td) : null;
String sourceFile = fqn != null ? context.getRelativePath(fqn) : null;
t.getActions().add(Action.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber, fqn, sourceFile));
}
}

View File

@@ -19,7 +19,9 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CodebaseContext {
private final Map<String, CompilationUnit> classes = new HashMap<>();
private final Map<String, Path> classPaths = new HashMap<>();
@@ -30,6 +32,23 @@ public class CodebaseContext {
private Map<String, Map<String, String>> allProperties = new HashMap<>();
private List<LibraryHint> libraryHints = new ArrayList<>();
private final ObjectMapper objectMapper = new ObjectMapper();
private Path projectRoot;
public void setProjectRoot(Path root) {
this.projectRoot = root;
}
public String getRelativePath(Path fullPath) {
if (projectRoot != null && fullPath.isAbsolute() && fullPath.startsWith(projectRoot)) {
return projectRoot.relativize(fullPath).toString();
}
return fullPath.getFileName().toString();
}
public String getRelativePath(String fqn) {
Path path = classPaths.get(fqn);
return path != null ? getRelativePath(path) : null;
}
public void loadLibraryHints(Path hintsFile) throws IOException {
if (Files.exists(hintsFile)) {
@@ -83,9 +102,10 @@ public class CodebaseContext {
activeIgnore.addAll(customIgnorePatterns);
List<Path> javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore);
log.info("CodebaseContext found {} java files across {} root directories", javaFiles.size(), rootDirs.size());
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
CompilationUnit cu = parse(source, javaFile.getFileName().toString());
CompilationUnit cu = parse(source, javaFile.toAbsolutePath().toString());
allCus.add(cu);
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
@@ -97,9 +117,9 @@ public class CodebaseContext {
}
}
private void indexType(TypeDeclaration td, String packageName, Path javaFile) {
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
String simpleName = td.getName().getIdentifier();
String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
classes.put(fqn, (CompilationUnit) td.getRoot());
classPaths.put(fqn, javaFile);
@@ -118,6 +138,13 @@ public class CodebaseContext {
String itfName = itf.toString();
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
}
// Recursively index nested types
for (Object type : td.getTypes()) {
if (type instanceof TypeDeclaration nestedTd) {
indexType(nestedTd, fqn, javaFile);
}
}
}
public List<String> getImplementations(String interfaceName) {
@@ -320,25 +347,38 @@ public class CodebaseContext {
}
public String getFqn(TypeDeclaration td) {
StringBuilder sb = new StringBuilder(td.getName().getIdentifier());
ASTNode current = td.getParent();
while (current instanceof TypeDeclaration parent) {
sb.insert(0, parent.getName().getIdentifier() + ".");
current = parent.getParent();
}
CompilationUnit cu = (CompilationUnit) td.getRoot();
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
String simpleName = td.getName().getIdentifier();
return packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
String fqn = sb.toString();
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
}
private TypeDeclaration findTypeInCu(CompilationUnit cu, String fqn) {
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
for (Object type : cu.types()) {
if (type instanceof TypeDeclaration td) {
String simpleName = td.getName().getIdentifier();
String typeFqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
if (typeFqn.equals(fqn))
return td;
TypeDeclaration found = findNestedType(td, fqn);
if (found != null) return found;
}
}
return null;
}
private TypeDeclaration findNestedType(TypeDeclaration td, String targetFqn) {
if (getFqn(td).equals(targetFqn)) return td;
for (TypeDeclaration nested : td.getTypes()) {
TypeDeclaration found = findNestedType(nested, targetFqn);
if (found != null) return found;
}
return null;
}
public Path getPath(String className) {
return classPaths.get(className);
}

View File

@@ -1,7 +1,7 @@
package click.kamil.springstatemachineexporter.model;
public record Action(String expression, boolean isLambda, String internalLogic, Integer lineNumber) {
public static Action of(String expression, boolean isLambda, String internalLogic, Integer lineNumber) {
return expression != null ? new Action(expression, isLambda, internalLogic, lineNumber) : null;
public record Action(String expression, boolean isLambda, String internalLogic, Integer lineNumber, String className, String sourceFile) {
public static Action of(String expression, boolean isLambda, String internalLogic, Integer lineNumber, String className, String sourceFile) {
return expression != null ? new Action(expression, isLambda, internalLogic, lineNumber, className, sourceFile) : null;
}
}

View File

@@ -1,7 +1,7 @@
package click.kamil.springstatemachineexporter.model;
public record Guard(String expression, boolean isLambda, String internalLogic, Integer lineNumber) {
public static Guard of(String expression, boolean isLambda, String internalLogic, Integer lineNumber) {
return expression != null ? new Guard(expression, isLambda, internalLogic, lineNumber) : null;
public record Guard(String expression, boolean isLambda, String internalLogic, Integer lineNumber, String className, String sourceFile) {
public static Guard of(String expression, boolean isLambda, String internalLogic, Integer lineNumber, String className, String sourceFile) {
return expression != null ? new Guard(expression, isLambda, internalLogic, lineNumber, className, sourceFile) : null;
}
}

View File

@@ -68,10 +68,23 @@ public class ExportService {
allPaths.add(inputDir);
allPaths.addAll(siblingResolver.resolveSiblings(inputDir));
context.setSourcepath(allPaths.stream()
// Find project root (common ancestor)
Path projectRoot = inputDir.toAbsolutePath();
for (Path p : allPaths) {
Path ap = p.toAbsolutePath();
while (projectRoot != null && !ap.startsWith(projectRoot)) {
projectRoot = projectRoot.getParent();
}
}
context.setProjectRoot(projectRoot);
log.info("Project root resolved to: {}", projectRoot);
List<String> sourcepaths = allPaths.stream()
.map(p -> p.resolve("src/main/java").toString())
.filter(p -> Files.exists(Path.of(p)))
.collect(Collectors.toList()));
.collect(Collectors.toList());
log.info("Setting JDT sourcepath: {}", sourcepaths);
context.setSourcepath(sourcepaths);
context.setResolveBindings(true);
Path hintsFile = inputDir.resolve("hints.json");