diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrerTest.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrerTest.java index 899b3e701..8df23a81a 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrerTest.java +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrerTest.java @@ -11,23 +11,35 @@ package com.avaloq.tools.ddk.xtext.export.jvmmodel; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.impl.ResourceImpl; import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.common.types.JvmDeclaredType; import org.eclipse.xtext.common.types.JvmGenericType; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.xbase.jvmmodel.AbstractModelInferrer; +import org.eclipse.xtext.xbase.jvmmodel.IJvmDeclaredTypeAcceptor; import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder; +import org.eclipse.xtext.xbase.lib.Procedures.Procedure1; import org.junit.jupiter.api.Test; +import com.avaloq.tools.ddk.xtext.export.export.ExportModel; +import com.avaloq.tools.ddk.xtext.expression.generator.GenModelUtilX; import com.avaloq.tools.ddk.xtext.test.export.util.ExportTestUtil; import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; @@ -37,6 +49,11 @@ *

* That header is optional, so {@code ExportModel.name} is {@code null} for such models. The documentation of the inferred * resource description manager must then be rendered with an empty name rather than with the string {@code "null"}. + *

+ *

+ * The inferrer must also leave the {@link GenModelUtilX} context as it found it, since a context left set keeps the + * model's resource reachable from the builder thread. + *

*/ @SuppressWarnings("nls") public class ExportJvmModelInferrerTest extends AbstractXtextTest { @@ -78,6 +95,73 @@ public void testResourceDescriptionManagerDocumentationOfHeaderlessModel() throw "A null export model name must render as an empty string, not as \"null\"."); } + @Test + public void testInferenceRestoresGenModelContext() throws IOException, ReflectiveOperationException { + final Resource resource = parseHeaderlessModel(); + final ExportModel model = (ExportModel) resource.getContents().get(0); + final ExportJvmModelInferrer inferrer = getXtextTestUtil().get(ExportJvmModelInferrer.class); + final GenModelUtilX genModelUtil = genModelUtilOf(inferrer); + final Resource previousContext = new ResourceImpl(URI.createURI("memory:/previous-context")); + genModelUtil.setResource(previousContext); + try { + final RecordingAcceptor acceptor = new RecordingAcceptor(resource); + // as the JVM model associator does before inferring; setContext is package-private + final Method setContext = AbstractModelInferrer.class.getDeclaredMethod("setContext", Resource.class); + setContext.setAccessible(true); + setContext.invoke(inferrer, resource); + inferrer.infer(model, acceptor, false); + assertSame(previousContext, genModelUtil.getContext(), "Inference must restore the previous GenModelUtilX context."); + assertFalse(acceptor.initializers.isEmpty(), "The inferrer must register type initializers."); + acceptor.initializers.forEach(Runnable::run); + assertSame(previousContext, genModelUtil.getContext(), "Type initializers must restore the previous GenModelUtilX context."); + } finally { + genModelUtil.setResource(null); + } + } + + /** + * Returns the {@link GenModelUtilX} the given inferrer sets its context on. + * + * @param inferrer + * the inferrer, must not be {@code null} + * @return the inferrer's utility, never {@code null} + * @throws ReflectiveOperationException + * if the field cannot be read + */ + private static GenModelUtilX genModelUtilOf(final ExportJvmModelInferrer inferrer) throws ReflectiveOperationException { + final Field field = ExportJvmModelInferrer.class.getDeclaredField("genModelUtil"); + field.setAccessible(true); + return (GenModelUtilX) field.get(inferrer); + } + + /** + * Adds the accepted types to the resource, as the JVM model associator does, and records their initializers so that the + * test can run them after inference. + */ + private static final class RecordingAcceptor implements IJvmDeclaredTypeAcceptor { + + private final Resource resource; + private final List initializers = new ArrayList<>(); + + RecordingAcceptor(final Resource resource) { + this.resource = resource; + } + + @Override + public IPostIndexingInitializing accept(final T type) { + resource.getContents().add(type); + return initializer -> initializers.add(() -> initializer.apply(type)); + } + + @Override + public void accept(final T type, final Procedure1 lateInitialization) { + resource.getContents().add(type); + if (lateInitialization != null) { + initializers.add(() -> lateInitialization.apply(type)); + } + } + } + /** * Parses the header-less export model and installs its derived state. *

diff --git a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrer.java b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrer.java index 2a13a7bed..961aa014a 100644 --- a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrer.java +++ b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrer.java @@ -166,9 +166,12 @@ protected void _infer(final ExportModel model, final IJvmDeclaredTypeAcceptor ac * Runs the given operation with the model's resource as the {@link GenModelUtilX} context, restoring the previous * context afterwards. *

- * The context is a thread-local on a shared {@link GenModelUtilX}. Leaving it set after inference would keep the - * model's resource, and through it the builder's whole resource set, reachable from a pooled builder thread until - * that thread happens to infer another export model. + * The context is a thread-local of this inferrer's own {@link GenModelUtilX}; the utility is not a singleton, so every + * inferrer instance has a separate thread-local. Left set after inference, the context would keep the model's resource, + * and through it the builder's whole resource set, reachable from a pooled builder thread for as long as that thread + * lives: the resource's inferred types hold body closures referencing this inferrer, so the thread-local entry keeps its + * own weakly referenced key reachable and is never cleared. Inferring another export model does not overwrite it either, + * since that happens through another inferrer and thread-local. *

* * @param model @@ -207,10 +210,10 @@ private Procedure1 inModelContext(final ExportModel model, final * the inferrer's invocation; running them from inside a deferred body closure delays that work past the * Xtext linking phase, by which time the grammar's parser rules and the model's import packages are linked. *

- * The injected {@link GenModelUtilX} is shared across resources and carries the current model's resource as - * mutable state; the inferrer sets it on entry, but by the time deferred bodies fire another resource may have - * inferred and clobbered the field. Re-bind it to this body's model resource so the GenPackage lookup runs in - * the right context. + * Deferred bodies run during generation, after {@link #_infer} has restored the previous context, so the model's resource + * is set as the context of this inferrer's {@link GenModelUtilX} while the producer runs, see + * {@link #withModelContext(ExportModel, Runnable)}. Only that instance carries the context: the expression compiler and + * translator inject their own instances, which look GenPackages up in the element's resource set instead. * * @param model * the export model the body belongs to, must not be {@code null}