stack overflow fixes
This commit is contained in:
6
TODO.md
6
TODO.md
@@ -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.
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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