diff --git a/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkInitializer.java b/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkInitializer.java index 4961c2b..b7d97db 100644 --- a/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkInitializer.java +++ b/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkInitializer.java @@ -92,7 +92,8 @@ public class JdkInitializer */ private Predicate registrationFilter = null; - private DiagnosticListener diagnosticListener = d -> {}; + private DiagnosticListener diagnosticListener = d -> { + }; private final List extraOptions = new ArrayList<>(); private boolean initialized = false; @@ -105,6 +106,14 @@ public class JdkInitializer private JdkExpressionConverter exprConverter; private JdkStatementConverter stmtConverter; + /** + * Body/initializer conversions deferred until every compilation unit's type descriptors have + * been registered, so that a call to a type processed later in source order still resolves. + * Each task re-establishes the expression converter's type context for its declaring class + * before converting, since that context is mutated while scanning subsequent classes. + */ + private final List deferredConversions = new ArrayList<>(); + /** * Creates a {@link JdkInitializer} with explicit source inputs. * @@ -286,6 +295,9 @@ public Void visitModule(final ModuleTree moduleTree, final Void unused) { }.scan(cut, null); } + deferredConversions.forEach(Runnable::run); + deferredConversions.clear(); + } catch (final IOException e) { initialized = false; throw new RuntimeException("Failed to initialize CodeModel from source files", e); @@ -347,7 +359,18 @@ private void processTypeElement(final TypeElement typeElement, if (!(typeElement.getEnclosingElement() instanceof TypeElement)) { addImports(typeDescriptor, cut); } - processMembers(typeDescriptor, classPath); + + final List bodyTasks = new ArrayList<>(); + processMembers(typeDescriptor, classPath, bodyTasks); + + if (!bodyTasks.isEmpty()) { + deferredConversions.add(() -> { + exprConverter.setTypeContext(trees, classPath.getCompilationUnit(), + mirror -> resolver.resolve(mirror, null)); + exprConverter.setEnclosingType(resolver.resolve(typeElement.asType(), null)); + bodyTasks.forEach(Runnable::run); + }); + } } private void addImports(final JDKTypeDescriptor typeDescriptor, final CompilationUnitTree cut) { @@ -371,7 +394,8 @@ private void addImports(final JDKTypeDescriptor typeDescriptor, final Compilatio } private void processMembers(final JDKTypeDescriptor typeDescriptor, - final TreePath classPath) { + final TreePath classPath, + final List bodyTasks) { final var cut = classPath.getCompilationUnit(); final var classTree = (ClassTree) classPath.getLeaf(); final var srcPositions = trees.getSourcePositions(); @@ -384,20 +408,20 @@ private void processMembers(final JDKTypeDescriptor typeDescriptor, final var elem = trees.getElement(path); if (member instanceof VariableTree vt && elem instanceof VariableElement ve) { if (ve.getKind() == ElementKind.FIELD) { - processField(typeDescriptor, ve, vt, cut); + processField(typeDescriptor, ve, vt, cut, bodyTasks); } else if (ve.getKind() == ElementKind.ENUM_CONSTANT) { final var name = nameProvider.getIrreducibleName(ve.getSimpleName()); typeDescriptor.addTrait(EnumConstantDescriptor.of(name, enumConstantOrder++)); } } else if (member instanceof MethodTree mt && elem instanceof ExecutableElement ee) { if (ee.getKind() == ElementKind.CONSTRUCTOR) { - processConstructor(typeDescriptor, ee, mt, cut); + processConstructor(typeDescriptor, ee, mt, cut, bodyTasks); } else if (ee.getKind() == ElementKind.METHOD) { - processMethod(typeDescriptor, ee, mt, cut); + processMethod(typeDescriptor, ee, mt, cut, bodyTasks); } } else if (member instanceof BlockTree bt) { - typeDescriptor.addTrait( - new InitializerBlockDescriptor(bt.isStatic(), stmtConverter.convertStatements(bt.getStatements()))); + bodyTasks.add(() -> typeDescriptor.addTrait( + new InitializerBlockDescriptor(bt.isStatic(), stmtConverter.convertStatements(bt.getStatements())))); } } } @@ -405,22 +429,24 @@ private void processMembers(final JDKTypeDescriptor typeDescriptor, private void processField(final JDKTypeDescriptor typeDescriptor, final VariableElement fieldElement, final VariableTree varTree, - final CompilationUnitTree cut) { + final CompilationUnitTree cut, + final List bodyTasks) { final var fieldDescriptor = resolver.createFieldDescriptor(fieldElement); addSourceLocation(cut, varTree, fieldDescriptor); typeDescriptor.addTrait(fieldDescriptor); if (varTree.getInitializer() != null) { - fieldDescriptor.addTrait( - new FieldInitializerDescriptor(exprConverter.convert(varTree.getInitializer()))); + bodyTasks.add(() -> fieldDescriptor.addTrait( + new FieldInitializerDescriptor(exprConverter.convert(varTree.getInitializer())))); } } private void processConstructor(final JDKTypeDescriptor typeDescriptor, final ExecutableElement methodElement, final MethodTree ctorTree, - final CompilationUnitTree cut) { + final CompilationUnitTree cut, + final List bodyTasks) { final var formalParameters = getFormalParameters(methodElement, ctorTree, cut); final var constructorDescriptor = ConstructorDescriptor.of(typeDescriptor, formalParameters); @@ -439,13 +465,14 @@ private void processConstructor(final JDKTypeDescriptor typeDescriptor, return pos != Diagnostic.NOPOS && pos > bodyStart; }) .toList(); - constructorDescriptor.addTrait(new MethodBodyDescriptor(stmtConverter.convertStatements(realStmts))); + bodyTasks.add(() -> constructorDescriptor.addTrait( + new MethodBodyDescriptor(stmtConverter.convertStatements(realStmts)))); } } private Stream getFormalParameters(final ExecutableElement methodElement, - final MethodTree methodTree, - final CompilationUnitTree cut) { + final MethodTree methodTree, + final CompilationUnitTree cut) { final var paramTrees = methodTree.getParameters(); final var index = new AtomicInteger(); return resolver.getFormalParameters(methodElement, (_, pd) -> { @@ -459,7 +486,8 @@ private Stream getFormalParameters(final ExecutableEl private void processMethod(final JDKTypeDescriptor typeDescriptor, final ExecutableElement methodElement, final MethodTree methodTree, - final CompilationUnitTree cut) { + final CompilationUnitTree cut, + final List bodyTasks) { final var returnType = resolver.resolve(methodElement.getReturnType(), methodElement); final var methodName = resolver.methodName(typeDescriptor, methodElement); @@ -473,7 +501,8 @@ private void processMethod(final JDKTypeDescriptor typeDescriptor, typeDescriptor.addTrait(methodDescriptor); if (methodTree.getBody() != null) { - methodDescriptor.addTrait(new MethodBodyDescriptor(stmtConverter.convertBlock(methodTree.getBody()))); + bodyTasks.add(() -> methodDescriptor.addTrait( + new MethodBodyDescriptor(stmtConverter.convertBlock(methodTree.getBody())))); } } diff --git a/jdk-codemodel/src/test/java/build/codemodel/jdk/TwoPassResolutionTests.java b/jdk-codemodel/src/test/java/build/codemodel/jdk/TwoPassResolutionTests.java new file mode 100644 index 0000000..253a679 --- /dev/null +++ b/jdk-codemodel/src/test/java/build/codemodel/jdk/TwoPassResolutionTests.java @@ -0,0 +1,93 @@ +package build.codemodel.jdk; + +import build.codemodel.foundation.naming.NonCachingNameProvider; +import build.codemodel.jdk.descriptor.MethodBodyDescriptor; +import build.codemodel.jdk.expression.MethodInvocation; +import build.codemodel.jdk.expression.ResolvedMethod; +import build.codemodel.objectoriented.descriptor.MethodDescriptor; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link JdkInitializer} resolves calls to types regardless of source-list order. + * Type descriptor registration and method/constructor/field-initializer body conversion happen in + * two separate passes: all compilation units are registered first, and body conversion is deferred + * until every type's descriptor exists, so a call to a type processed later in source order still + * resolves to a {@link ResolvedMethod} trait. + * + *

{@code Caller#outer()} calls {@code Callee#helper()}. Listing {@code caller} before + * {@code callee} reproduces the bug; swapping the order makes it pass. + */ +class TwoPassResolutionTests { + + private static final String CALLEE_SOURCE = """ + package com.example; + public class Callee { + public static String helper() { + return "helper"; + } + } + """; + + private static final String CALLER_SOURCE = """ + package com.example; + public class Caller { + public String outer() { + return Callee.helper(); + } + } + """; + + private static JavaFileObject inMemorySource(final String qualifiedName, final String source) { + final var uri = URI.create("mem:///" + qualifiedName.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension); + return new SimpleJavaFileObject(uri, JavaFileObject.Kind.SOURCE) { + @Override + public CharSequence getCharContent(final boolean ignoreEncodingErrors) { + return source; + } + }; + } + + private static boolean helperCallResolves(final List sourcesInProcessingOrder) { + final var nameProvider = new NonCachingNameProvider(); + final var codeModel = new JDKCodeModel(nameProvider); + new JdkInitializer(List.of(), List.of(), sourcesInProcessingOrder).initialize(codeModel); + + final var callerType = codeModel.getTypeDescriptor( + nameProvider.getTypeName(java.util.Optional.empty(), "com.example.Caller")).orElseThrow(); + + final var outer = callerType.traits(MethodDescriptor.class) + .filter(md -> md.methodName().name().toString().equals("outer")) + .findFirst().orElseThrow(); + + final var body = outer.getTrait(MethodBodyDescriptor.class).orElseThrow().body(); + + final var helperCall = body.composition(MethodInvocation.class) + .filter(mi -> mi.methodName().equals("helper")) + .findFirst().orElseThrow(); + + return helperCall.getTrait(ResolvedMethod.class).isPresent(); + } + + @Test + void helperCallResolvesToResolveWhenCallerIsProcessedBeforeCallee() { + final var caller = inMemorySource("com.example.Caller", CALLER_SOURCE); + final var callee = inMemorySource("com.example.Callee", CALLEE_SOURCE); + + assertThat(helperCallResolves(List.of(caller, callee))).isTrue(); + } + + @Test + void helperCallResolvesWhenCalleeIsProcessedBeforeCaller() { + final var caller = inMemorySource("com.example.Caller", CALLER_SOURCE); + final var callee = inMemorySource("com.example.Callee", CALLEE_SOURCE); + + assertThat(helperCallResolves(List.of(callee, caller))).isTrue(); + } +}