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

View File

@@ -20,6 +20,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -28,6 +29,7 @@ public class CodebaseContext {
private final Map<String, CompilationUnit> classes = new HashMap<>();
private final Map<String, Path> classPaths = new HashMap<>();
private final Map<String, String> simpleNameToFqn = new HashMap<>();
private final Set<String> ambiguousSimpleNames = new HashSet<>();
public void scan(Path rootDir) throws IOException {
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
@@ -41,7 +43,15 @@ public class CodebaseContext {
String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
classes.put(fqn, cu);
classPaths.put(fqn, javaFile);
simpleNameToFqn.put(simpleName, fqn);
if (simpleNameToFqn.containsKey(simpleName)) {
String existingFqn = simpleNameToFqn.get(simpleName);
if (!existingFqn.equals(fqn)) {
ambiguousSimpleNames.add(simpleName);
}
} else {
simpleNameToFqn.put(simpleName, fqn);
}
}
}
}
@@ -101,7 +111,13 @@ public class CodebaseContext {
if (cu != null)
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);
if (fqn != null) {
cu = classes.get(fqn);
@@ -226,6 +242,16 @@ public class CodebaseContext {
}
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");
CompilationUnit cu = (CompilationUnit) td.getRoot();
@@ -236,7 +262,7 @@ public class CodebaseContext {
if (knownAdapters.contains(superclassName))
return true;
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd))
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
return true;
}
@@ -247,7 +273,7 @@ public class CodebaseContext {
if (knownAdapters.contains(interfaceName))
return true;
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd))
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
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();
}
}