stack overflow fixes

This commit is contained in:
2026-06-08 18:28:08 +02:00
parent a4a3493b42
commit ebb9fb5425
4 changed files with 239 additions and 60 deletions

View File

@@ -1,3 +1,9 @@
1.extract from all possible formats (ask chatgpt) 1.extract from all possible formats (ask chatgpt)
json, xml, maybe yaml json, xml, maybe yaml
2. [PLAN] Support external dependencies (JARs/compiled classes):
- Enable binding resolution in ASTParser (`setResolveBindings(true)`).
- Configure ASTParser with project classpath (including JARs).
- Refactor `CodebaseContext` to handle resolved bindings, not just local AST nodes.
- Implement tests using external libraries as base classes.

View File

@@ -50,9 +50,12 @@ public class StateMachineAggregator {
} }
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) { private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) if (currentTd == null)
return; return;
visitedClasses.add(currentTd.getName().getFullyQualifiedName()); String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) { for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureTransitionsMethod(method)) { if (isConfigureTransitionsMethod(method)) {
@@ -233,7 +236,13 @@ public class StateMachineAggregator {
} }
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) { private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
if (expr instanceof MethodInvocation mi) { Set<Expression> visited = new HashSet<>();
Expression current = expr;
while (current != null) {
if (!visited.add(current))
break;
if (current instanceof MethodInvocation mi) {
String name = mi.getName().getIdentifier(); String name = mi.getName().getIdentifier();
Expression receiver = mi.getExpression(); Expression receiver = mi.getExpression();
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List"))) if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
@@ -249,15 +258,19 @@ public class StateMachineAggregator {
return (List<Expression>) mi.arguments(); return (List<Expression>) mi.arguments();
} }
} }
if (expr instanceof SimpleName sn) { if (current instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier()); Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list) if (resolved instanceof List list)
return (List<Expression>) list; return (List<Expression>) list;
if (resolved instanceof Expression e) if (resolved instanceof Expression e) {
return unrollCollection(e, argsMap); current = e;
continue;
} }
if (expr instanceof ArrayInitializer ai) }
if (current instanceof ArrayInitializer ai)
return (List<Expression>) ai.expressions(); return (List<Expression>) ai.expressions();
break;
}
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -375,15 +388,17 @@ public class StateMachineAggregator {
private Object resolve(Expression expr, Map<String, Object> argsMap) { private Object resolve(Expression expr, Map<String, Object> argsMap) {
if (expr instanceof NullLiteral) if (expr instanceof NullLiteral)
return null; return null;
if (expr instanceof SimpleName sn) { Set<Expression> visited = new HashSet<>();
Object current = expr;
while (current instanceof SimpleName sn) {
if (!visited.add((SimpleName) current))
break;
Object resolved = argsMap.get(sn.getIdentifier()); Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof Expression nextExpr && nextExpr != expr) { if (resolved == null || resolved == current)
return resolve(nextExpr, argsMap); break;
current = resolved;
} }
if (resolved != null) return current;
return resolved;
}
return expr;
} }
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) { private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
@@ -403,37 +418,64 @@ public class StateMachineAggregator {
} }
private boolean isTransitionChainEntry(MethodInvocation mi) { private boolean isTransitionChainEntry(MethodInvocation mi) {
String name = mi.getName().getIdentifier(); Expression current = mi;
while (current instanceof MethodInvocation call) {
String name = call.getName().getIdentifier();
if (TransitionType.fromMethodName(name).isPresent()) if (TransitionType.fromMethodName(name).isPresent())
return true; return true;
Expression receiver = mi.getExpression(); current = call.getExpression();
if (receiver instanceof MethodInvocation next) }
return isTransitionChainEntry(next);
return false; return false;
} }
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) { private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
String name = mi.getName().getIdentifier(); Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
String name = call.getName().getIdentifier();
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) { if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
Expression receiver = mi.getExpression(); Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) if (receiver instanceof MethodInvocation next) {
return isTransitionChainContinuation(next, argsMap) || isTransitionChainEntry(next); current = next;
continue;
}
Object resolved = resolve(receiver, argsMap); Object resolved = resolve(receiver, argsMap);
return resolved instanceof MethodInvocation || resolved == null; // null means it's a parameter we track if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
return resolved == null; // null means it's a parameter we track
}
if (TransitionType.fromMethodName(name).isPresent())
return true;
break;
} }
return false; return false;
} }
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) { private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
return findMethodInHierarchy(methodName, td, new HashSet<>());
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
if (td == null) if (td == null)
return null; return null;
String fqn = context.getFqn(td);
if (visited.contains(fqn))
return null;
visited.add(fqn);
for (MethodDeclaration method : td.getMethods()) for (MethodDeclaration method : td.getMethods())
if (method.getName().getIdentifier().equals(methodName)) if (method.getName().getIdentifier().equals(methodName))
return method; return method;
Type superclassType = td.getSuperclassType(); Type superclassType = td.getSuperclassType();
if (superclassType != null) { if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType)); TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
return findMethodInHierarchy(methodName, superclassTd); return findMethodInHierarchy(methodName, superclassTd, visited);
} }
return null; return null;
} }
@@ -466,9 +508,12 @@ public class StateMachineAggregator {
} }
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) { private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) if (currentTd == null)
return; return;
visitedClasses.add(currentTd.getName().getFullyQualifiedName()); String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) { for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureStatesMethod(method)) { if (isConfigureStatesMethod(method)) {
@@ -509,14 +554,25 @@ public class StateMachineAggregator {
} }
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) { private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
if (mi.getName().getIdentifier().equals("withStates")) Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
if (call.getName().getIdentifier().equals("withStates"))
return true; return true;
Expression receiver = mi.getExpression(); Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) if (receiver instanceof MethodInvocation next) {
return isWithStatesChain(next, argsMap); current = next;
continue;
}
Object resolved = resolve(receiver, argsMap); Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) if (resolved instanceof MethodInvocation next) {
return isWithStatesChain(next, argsMap); current = next;
continue;
}
break;
}
return false; return false;
} }

View File

@@ -20,6 +20,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -28,6 +29,7 @@ public class CodebaseContext {
private final Map<String, CompilationUnit> classes = new HashMap<>(); private final Map<String, CompilationUnit> classes = new HashMap<>();
private final Map<String, Path> classPaths = new HashMap<>(); private final Map<String, Path> classPaths = new HashMap<>();
private final Map<String, String> simpleNameToFqn = new HashMap<>(); private final Map<String, String> simpleNameToFqn = new HashMap<>();
private final Set<String> ambiguousSimpleNames = new HashSet<>();
public void scan(Path rootDir) throws IOException { public void scan(Path rootDir) throws IOException {
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java"); List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
@@ -41,11 +43,19 @@ public class CodebaseContext {
String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName; String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
classes.put(fqn, cu); classes.put(fqn, cu);
classPaths.put(fqn, javaFile); classPaths.put(fqn, javaFile);
if (simpleNameToFqn.containsKey(simpleName)) {
String existingFqn = simpleNameToFqn.get(simpleName);
if (!existingFqn.equals(fqn)) {
ambiguousSimpleNames.add(simpleName);
}
} else {
simpleNameToFqn.put(simpleName, fqn); simpleNameToFqn.put(simpleName, fqn);
} }
} }
} }
} }
}
public State resolveState(Expression expr, CompilationUnit cu) { public State resolveState(Expression expr, CompilationUnit cu) {
if (expr == null) if (expr == null)
@@ -101,7 +111,13 @@ public class CodebaseContext {
if (cu != null) if (cu != null)
return findTypeInCu(cu, name); return findTypeInCu(cu, name);
// If not found, it might be a simple name // If it's a simple name, check if it's ambiguous
if (ambiguousSimpleNames.contains(name)) {
// System.err.println("Warning: Ambiguous simple name '" + name + "'. Provide FQN to resolve correctly.");
return null; // Don't return a random class if it's ambiguous
}
// If not ambiguous, return the one we found during scan
String fqn = simpleNameToFqn.get(name); String fqn = simpleNameToFqn.get(name);
if (fqn != null) { if (fqn != null) {
cu = classes.get(fqn); cu = classes.get(fqn);
@@ -226,6 +242,16 @@ public class CodebaseContext {
} }
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) { public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
}
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td, Set<String> visited) {
String fqn = getFqn(td);
if (visited.contains(fqn)) {
return false;
}
visited.add(fqn);
Set<String> knownAdapters = Set.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter", "StateMachineConfigurer"); Set<String> knownAdapters = Set.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter", "StateMachineConfigurer");
CompilationUnit cu = (CompilationUnit) td.getRoot(); CompilationUnit cu = (CompilationUnit) td.getRoot();
@@ -236,7 +262,7 @@ public class CodebaseContext {
if (knownAdapters.contains(superclassName)) if (knownAdapters.contains(superclassName))
return true; return true;
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu); TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd)) if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
return true; return true;
} }
@@ -247,7 +273,7 @@ public class CodebaseContext {
if (knownAdapters.contains(interfaceName)) if (knownAdapters.contains(interfaceName))
return true; return true;
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu); TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd)) if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
return true; return true;
} }
} }

View File

@@ -0,0 +1,91 @@
package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
public class ResolutionRobustnessTest {
@Test
void testStackOverflowPreventionWithCyclicInheritance(@TempDir Path tempDir) throws IOException {
Path fooDir = tempDir.resolve("com/foo");
Files.createDirectories(fooDir);
// Cyclic inheritance: Config extends Config
Files.writeString(fooDir.resolve("Config.java"),
"package com.foo;\n" +
"public class Config extends Config {\n" +
" public void configure() {}\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Config");
assertThat(td).isNotNull();
// This should not throw StackOverflowError
boolean result = context.extendsStateMachineConfigurerAdapter(td);
assertThat(result).isFalse();
}
@Test
void testShadowingResolution(@TempDir Path tempDir) throws IOException {
// Package com.foo has a class Config
Path fooDir = tempDir.resolve("com/foo");
Files.createDirectories(fooDir);
Files.writeString(fooDir.resolve("Config.java"), "package com.foo; public class Config {}");
// Package com.bar also has a class Config
Path barDir = tempDir.resolve("com/bar");
Files.createDirectories(barDir);
Files.writeString(barDir.resolve("Config.java"), "package com.bar; public class Config {}");
// App class imports com.bar.Config
Path appDir = tempDir.resolve("com/app");
Files.createDirectories(appDir);
String appSource = "package com.app;\n" +
"import com.bar.Config;\n" +
"public class App extends Config {}";
Path appFile = appDir.resolve("App.java");
Files.writeString(appFile, appSource);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration appTd = context.getTypeDeclaration("com.app.App");
assertThat(appTd).isNotNull();
// Resolution in App.java context should pick com.bar.Config due to import
TypeDeclaration resolvedTd = context.getTypeDeclaration("Config", (CompilationUnit) appTd.getRoot());
assertThat(context.getFqn(resolvedTd)).isEqualTo("com.bar.Config");
}
@Test
void testAmbiguityHandling(@TempDir Path tempDir) throws IOException {
// Two classes with same name in different packages, no imports in context
Files.createDirectories(tempDir.resolve("p1"));
Files.writeString(tempDir.resolve("p1/X.java"), "package p1; public class X {}");
Files.createDirectories(tempDir.resolve("p2"));
Files.writeString(tempDir.resolve("p2/X.java"), "package p2; public class X {}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
// Ambiguous simple name should return null to prevent incorrect resolution
TypeDeclaration td = context.getTypeDeclaration("X");
assertThat(td).isNull();
// Exact FQN should still work
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
}
}