stack overflow fixes
This commit is contained in:
8
TODO.md
8
TODO.md
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user