Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions jdk-codemodel/src/main/java/build/codemodel/jdk/JdkInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public class JdkInitializer
*/
private Predicate<URI> registrationFilter = null;

private DiagnosticListener<JavaFileObject> diagnosticListener = d -> {};
private DiagnosticListener<JavaFileObject> diagnosticListener = d -> {
};
private final List<String> extraOptions = new ArrayList<>();

private boolean initialized = false;
Expand All @@ -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<Runnable> deferredConversions = new ArrayList<>();

/**
* Creates a {@link JdkInitializer} with explicit source inputs.
*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -347,7 +359,18 @@ private void processTypeElement(final TypeElement typeElement,
if (!(typeElement.getEnclosingElement() instanceof TypeElement)) {
addImports(typeDescriptor, cut);
}
processMembers(typeDescriptor, classPath);

final List<Runnable> 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) {
Expand All @@ -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<Runnable> bodyTasks) {
final var cut = classPath.getCompilationUnit();
final var classTree = (ClassTree) classPath.getLeaf();
final var srcPositions = trees.getSourcePositions();
Expand All @@ -384,43 +408,45 @@ 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()))));
}
}
}

private void processField(final JDKTypeDescriptor typeDescriptor,
final VariableElement fieldElement,
final VariableTree varTree,
final CompilationUnitTree cut) {
final CompilationUnitTree cut,
final List<Runnable> 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<Runnable> bodyTasks) {
final var formalParameters = getFormalParameters(methodElement, ctorTree, cut);

final var constructorDescriptor = ConstructorDescriptor.of(typeDescriptor, formalParameters);
Expand All @@ -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<FormalParameterDescriptor> 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) -> {
Expand All @@ -459,7 +486,8 @@ private Stream<FormalParameterDescriptor> getFormalParameters(final ExecutableEl
private void processMethod(final JDKTypeDescriptor typeDescriptor,
final ExecutableElement methodElement,
final MethodTree methodTree,
final CompilationUnitTree cut) {
final CompilationUnitTree cut,
final List<Runnable> bodyTasks) {
final var returnType = resolver.resolve(methodElement.getReturnType(), methodElement);
final var methodName = resolver.methodName(typeDescriptor, methodElement);

Expand All @@ -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()))));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>{@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<JavaFileObject> 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();
}
}