better finding state machines
This commit is contained in:
@@ -29,10 +29,12 @@ public class StateMachineAggregator {
|
||||
allTransitions.addAll(parseTransitionsWithMethodCalls(configureMethod, searchStartClass, visitedMethods, argsMap));
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||
|
||||
// Hierarchy - Superclass
|
||||
Type superclassType = currentTd.getSuperclassType();
|
||||
if (superclassType != null) {
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||
if (superclassTd != null) {
|
||||
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
@@ -41,7 +43,7 @@ public class StateMachineAggregator {
|
||||
// Hierarchy - Interfaces
|
||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType));
|
||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||
if (interfaceTd != null) {
|
||||
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
@@ -376,17 +378,19 @@ public class StateMachineAggregator {
|
||||
MethodDeclaration configureMethod = findConfigureStatesMethod(currentTd);
|
||||
if (configureMethod != null) parseStatesWithMethodCalls(configureMethod, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||
|
||||
// Hierarchy - Superclass
|
||||
Type superclassType = currentTd.getSuperclassType();
|
||||
if (superclassType != null) {
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||
if (superclassTd != null) aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
|
||||
// Hierarchy - Interfaces
|
||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType));
|
||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||
if (interfaceTd != null) aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ public final class AstUtils {
|
||||
}
|
||||
public static String extractSimpleTypeName(Type type) {
|
||||
if (type.isSimpleType()) {
|
||||
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
Name name = ((SimpleType) type).getName();
|
||||
if (name instanceof QualifiedName qn) {
|
||||
return qn.getName().getIdentifier();
|
||||
}
|
||||
return name.toString();
|
||||
} else if (type.isParameterizedType()) {
|
||||
Type rawType = ((ParameterizedType) type).getType();
|
||||
return extractSimpleTypeName(rawType);
|
||||
|
||||
@@ -9,20 +9,22 @@ import java.util.*;
|
||||
|
||||
public class CodebaseContext {
|
||||
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
||||
private final Map<String, String> classSources = new HashMap<>();
|
||||
private final Map<String, Path> classPaths = new HashMap<>();
|
||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||
|
||||
public void scan(Path rootDir) throws IOException {
|
||||
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
|
||||
for (Path javaFile : javaFiles) {
|
||||
String source = Files.readString(javaFile);
|
||||
CompilationUnit cu = parse(source);
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
for (Object type : cu.types()) {
|
||||
if (type instanceof TypeDeclaration td) {
|
||||
String className = td.getName().getFullyQualifiedName();
|
||||
classes.put(className, cu);
|
||||
classSources.put(className, source);
|
||||
classPaths.put(className, javaFile);
|
||||
String simpleName = td.getName().getIdentifier();
|
||||
String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
|
||||
classes.put(fqn, cu);
|
||||
classPaths.put(fqn, javaFile);
|
||||
simpleNameToFqn.put(simpleName, fqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,12 +38,67 @@ public class CodebaseContext {
|
||||
return (CompilationUnit) parser.createAST(null);
|
||||
}
|
||||
|
||||
public TypeDeclaration getTypeDeclaration(String className) {
|
||||
CompilationUnit cu = classes.get(className);
|
||||
if (cu == null) return null;
|
||||
public TypeDeclaration getTypeDeclaration(String name) {
|
||||
// Try exact FQN match first
|
||||
CompilationUnit cu = classes.get(name);
|
||||
if (cu != null) return findTypeInCu(cu, name);
|
||||
|
||||
// If not found, it might be a simple name
|
||||
String fqn = simpleNameToFqn.get(name);
|
||||
if (fqn != null) {
|
||||
cu = classes.get(fqn);
|
||||
if (cu != null) return findTypeInCu(cu, fqn);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public TypeDeclaration getTypeDeclaration(String name, CompilationUnit contextCu) {
|
||||
if (name == null || name.isEmpty()) return null;
|
||||
|
||||
// 1. Check if it's already an FQN
|
||||
if (classes.containsKey(name)) return getTypeDeclaration(name);
|
||||
|
||||
// 2. Check imports in contextCu
|
||||
if (contextCu != null) {
|
||||
for (Object impObj : contextCu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (!imp.isStatic() && !imp.isOnDemand()) {
|
||||
if (impName.endsWith("." + name)) return getTypeDeclaration(impName);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check same package
|
||||
String packageName = contextCu.getPackage() != null ? contextCu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
String localFqn = packageName.isEmpty() ? name : packageName + "." + name;
|
||||
if (classes.containsKey(localFqn)) return getTypeDeclaration(localFqn);
|
||||
|
||||
// 4. Check on-demand imports (star imports)
|
||||
for (Object impObj : contextCu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
if (imp.isOnDemand()) {
|
||||
String starFqn = imp.getName().getFullyQualifiedName() + "." + name;
|
||||
if (classes.containsKey(starFqn)) return getTypeDeclaration(starFqn);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Fallback to java.lang (common)
|
||||
String langFqn = "java.lang." + name;
|
||||
if (classes.containsKey(langFqn)) return getTypeDeclaration(langFqn);
|
||||
}
|
||||
|
||||
// 6. Last resort: global simple name match (might be ambiguous but better than nothing)
|
||||
return getTypeDeclaration(name);
|
||||
}
|
||||
|
||||
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 && td.getName().getFullyQualifiedName().equals(className)) {
|
||||
return td;
|
||||
if (type instanceof TypeDeclaration td) {
|
||||
String simpleName = td.getName().getIdentifier();
|
||||
String typeFqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
|
||||
if (typeFqn.equals(fqn)) return td;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -88,13 +145,14 @@ public class CodebaseContext {
|
||||
|
||||
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||
Set<String> knownAdapters = Set.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter", "StateMachineConfigurer");
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) td.getRoot();
|
||||
|
||||
// Check superclass
|
||||
Type superclass = td.getSuperclassType();
|
||||
if (superclass != null) {
|
||||
String superclassName = getSimpleName(superclass);
|
||||
if (knownAdapters.contains(superclassName)) return true;
|
||||
TypeDeclaration superTd = getTypeDeclaration(superclassName);
|
||||
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
||||
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd)) return true;
|
||||
}
|
||||
|
||||
@@ -103,7 +161,7 @@ public class CodebaseContext {
|
||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||
String interfaceName = getSimpleName(interfaceType);
|
||||
if (knownAdapters.contains(interfaceName)) return true;
|
||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName);
|
||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
||||
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,30 @@ public class RegressionTest {
|
||||
Path.of("../state_machines/inheritance_extra_functions2_state_machine"),
|
||||
Path.of("src/test/resources/golden/ThreeStateMachineConfiguration"),
|
||||
"ThreeStateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"F1 State Machine (Example 4)",
|
||||
Path.of("../state_machines/inheritance_extra_functions3_state_machine"),
|
||||
Path.of("src/test/resources/golden/F1StateMachineConfiguration"),
|
||||
"F1StateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"F2 State Machine (Example 4)",
|
||||
Path.of("../state_machines/inheritance_extra_functions3_state_machine"),
|
||||
Path.of("src/test/resources/golden/F2StateMachineConfiguration"),
|
||||
"F2StateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"G1 State Machine (Example 4)",
|
||||
Path.of("../state_machines/inheritance_extra_functions3_state_machine"),
|
||||
Path.of("src/test/resources/golden/G1StateMachineConfiguration"),
|
||||
"G1StateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"G2 State Machine (Example 4)",
|
||||
Path.of("../state_machines/inheritance_extra_functions3_state_machine"),
|
||||
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
||||
"G2StateMachineConfiguration"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
@@ -273,21 +274,38 @@ class AstFileFinderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenClassHasAnnotationWithParameters() {
|
||||
String classWithAnnotatedParams = """
|
||||
@Service("testService")
|
||||
public class TestService {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
void shouldReturnTrue_whenFunctionReturnsTargetType() {
|
||||
String source = """
|
||||
public class Test {
|
||||
public StateMachine<S, E> build() { return null; }
|
||||
}
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(classWithAnnotatedParams, annotationNames);
|
||||
|
||||
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenNoFunctionReturnsTargetType() {
|
||||
String source = """
|
||||
public class Test {
|
||||
public void build() {}
|
||||
}
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenFunctionReturnsQualifiedTargetType() {
|
||||
String source = """
|
||||
public class Test {
|
||||
public org.springframework.statemachine.StateMachine build() { return null; }
|
||||
}
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user