From f487c0de1203dfa7f76c1efe7c4bae46b330aafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sat, 26 Sep 2026 13:45:50 +0200 Subject: [PATCH 1/2] fix(export,scope): record the Java types a model names as dependencies Java types named in `extension` and `inject` declarations and in expressions (casts, collection element types, receivers of static calls) are resolved only while the Java is generated, so they were neither imported names nor references of an export or scope model, and a change to such a type did not re-queue the model. Export and Scope now bind resource description managers, sharing a base class in the expression bundle, that add these types to the model's imported names in the form in which changed Java types are reported. For Scope this includes the types named by included scope models, which the generated provider uses as well. Fixes #1554 Co-Authored-By: Claude Opus 5.5 --- .../ExportResourceDescriptionManagerTest.java | 190 ++++++++++++++++++ .../xtext/test/export/ExportTestSuite.java | 3 +- .../META-INF/MANIFEST.MF | 3 +- .../ddk/xtext/export/ExportRuntimeModule.java | 12 ++ .../ExportResourceDescriptionManager.java | 46 +++++ .../META-INF/MANIFEST.MF | 1 + ...essionModelResourceDescriptionManager.java | 90 +++++++++ .../META-INF/MANIFEST.MF | 1 + .../ScopeResourceDescriptionManagerTest.java | 170 ++++++++++++++++ .../test/generator/GeneratorTestSuite.java | 2 + .../ddk/xtext/scope/ScopeRuntimeModule.java | 12 ++ .../ScopeResourceDescriptionManager.java | 76 +++++++ 12 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java create mode 100644 com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManager.java create mode 100644 com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/resource/AbstractExpressionModelResourceDescriptionManager.java create mode 100644 com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/expression/ScopeResourceDescriptionManagerTest.java create mode 100644 com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/resource/ScopeResourceDescriptionManager.java diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java new file mode 100644 index 000000000..6528760d5 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.export.resource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.InternalEObject; +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.JvmGenericType; +import org.eclipse.xtext.common.types.TypesFactory; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.EObjectDescription; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.IReferenceDescription; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.resource.impl.AbstractResourceDescription; +import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionDelta; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; +import org.junit.jupiter.api.Test; + +import com.avaloq.tools.ddk.xtext.test.export.util.ExportTestUtil; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; + + +/** + * Tests that the classes named in {@code extension} declarations are dependencies of an export model: a change to such a + * class must make the model affected. + */ +@SuppressWarnings("nls") +public class ExportResourceDescriptionManagerTest extends AbstractXtextTest { + + /** Fully qualified name of the extension class; it need not exist, since the dependency is recorded by name. */ + private static final String EXTENSION_CLASS = "com.acme.export.util.NamingExtensions"; + + private static final String WITH_EXTENSION = "WithExtension"; + + private static final String ECORE_IMPORT = "import \"http://www.eclipse.org/emf/2002/Ecore\" as ecore\n\n"; + + private static final String ECLASS_EXPORT = "export ecore::EClass as name\n"; + + private static final String MODEL_WITH_EXTENSION = ECORE_IMPORT + "extension com::acme::^export::util::NamingExtensions\n\n" + ECLASS_EXPORT; + + private static final String MODEL_WITHOUT_EXTENSION = ECORE_IMPORT + ECLASS_EXPORT; + + /** Fully qualified names of the types named in the casts of {@link #MODEL_WITH_CAST}; they need not exist either. */ + private static final String CAST_TYPE = "com.acme.export.model.Node"; + + private static final String ELEMENT_TYPE = "com.acme.export.model.Item"; + + private static final String MODEL_WITH_CAST = ECORE_IMPORT + "export ecore::EClass as name {\n" + + " data node = (com::acme::^export::model::Node) this, items = (List[com::acme::^export::model::Item]) this;\n}\n"; + + @Override + protected ExportTestUtil getXtextTestUtil() { + return ExportTestUtil.getInstance(); + } + + /** + * This test builds its sources in memory and has no test source file. {@inheritDoc} + */ + @Override + protected String getTestSourceFileName() { + return null; + } + + @Test + public void testExtensionClassIsImportedName() throws IOException { + final XtextResource resource = parse(WITH_EXTENSION, MODEL_WITH_EXTENSION); + final IResourceDescription description = resource.getResourceServiceProvider().getResourceDescriptionManager().getResourceDescription(resource); + assertTrue(imports(description, EXTENSION_CLASS), "The extension class must be an imported name of the export model."); + } + + @Test + public void testChangedExpressionTypeAffectsModel() throws IOException { + final XtextResource resource = parse("WithCast", MODEL_WITH_CAST); + assertTrue(resource.getErrors().isEmpty(), () -> "The model must parse: " + resource.getErrors()); + final IResourceDescription.Manager manager = resource.getResourceServiceProvider().getResourceDescriptionManager(); + final IResourceDescription candidate = manager.getResourceDescription(resource); + assertTrue(imports(candidate, CAST_TYPE), "A type named in a cast must be an imported name of the export model."); + assertTrue(imports(candidate, ELEMENT_TYPE), "The element type of a collection type must be an imported name of the export model."); + assertTrue(manager.isAffected(List.of(javaTypeChange(CAST_TYPE)), candidate, new ResourceDescriptionsData(List.of(candidate))), + "A change to a type named in an expression must affect the export model."); + } + + @Test + public void testChangedExtensionClassAffectsModel() throws IOException { + final XtextResource resource = parse(WITH_EXTENSION, MODEL_WITH_EXTENSION); + final IResourceDescription.Manager manager = resource.getResourceServiceProvider().getResourceDescriptionManager(); + assertInstanceOf(ExportResourceDescriptionManager.class, manager, "Export must bind its own resource description manager."); + final IResourceDescription candidate = manager.getResourceDescription(resource); + assertTrue(manager.isAffected(List.of(javaTypeChange(EXTENSION_CLASS)), candidate, new ResourceDescriptionsData(List.of(candidate))), + "A change to an extension class must affect the export model."); + } + + @Test + public void testChangedClassDoesNotAffectModelWithoutExtension() throws IOException { + final XtextResource resource = parse("WithoutExtension", MODEL_WITHOUT_EXTENSION); + final IResourceDescription.Manager manager = resource.getResourceServiceProvider().getResourceDescriptionManager(); + final IResourceDescription candidate = manager.getResourceDescription(resource); + assertFalse(manager.isAffected(List.of(javaTypeChange(EXTENSION_CLASS)), candidate, new ResourceDescriptionsData(List.of(candidate))), + "A class the export model does not declare must not affect it."); + } + + + /** + * Returns a change of the given Java type shaped like the ones JDT reports: the resource URI {@code java:/Objects/}, + * exporting one object named by the fully qualified name. + */ + private static IResourceDescription.Delta javaTypeChange(final String fqn) { + final URI uri = URI.createURI("java:/Objects/" + fqn); + final JvmGenericType type = TypesFactory.eINSTANCE.createJvmGenericType(); + ((InternalEObject) type).eSetProxyURI(uri.appendFragment(fqn)); + final IEObjectDescription exported = EObjectDescription.create(QualifiedName.create(fqn.split("\\.")), type); + final IResourceDescription changed = new AbstractResourceDescription() { + @Override + protected List computeExportedObjects() { + return List.of(exported); + } + + @Override + public Iterable getImportedNames() { + return List.of(); + } + + @Override + public Iterable getReferenceDescriptions() { + return List.of(); + } + + @Override + public URI getURI() { + return uri; + } + }; + return new DefaultResourceDescriptionDelta(null, changed); + } + + private static boolean imports(final IResourceDescription description, final String fqn) { + final QualifiedName expected = QualifiedName.create(fqn.split("\\.")).toLowerCase(); + for (final QualifiedName name : description.getImportedNames()) { + if (expected.equals(name)) { + return true; + } + } + return false; + } + + /** + * Parses the given export model with its derived state installed. The inferrer resolves the grammar of an export model by + * loading the {@code .xtext} file next to it, so an empty resource is registered under that URI. + */ + private XtextResource parse(final String name, final String source) throws IOException { + final URI modelUri = getTargetSourceUri(name + ".export"); + final URI grammarUri = modelUri.trimFileExtension().appendFileExtension("xtext"); + final Resource grammarResource = new ResourceImpl(grammarUri) { + @Override + public boolean isLoaded() { + return true; + } + }; + final XtextResourceSet resourceSet = getXtextTestUtil().getResourceSet(); + resourceSet.getResources().add(grammarResource); + resourceSet.getURIResourceMap().put(grammarUri, grammarResource); + final XtextResource resource = (XtextResource) resourceSet.createResource(modelUri); + resourceSet.getResources().add(resource); + resource.load(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)), null); + EcoreUtil.resolveAll(resource); + return resource; + } +} diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/ExportTestSuite.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/ExportTestSuite.java index 92cf96062..6eafeed48 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/ExportTestSuite.java +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/ExportTestSuite.java @@ -15,6 +15,7 @@ import com.avaloq.tools.ddk.xtext.export.formatting.ExportFormattingTest; import com.avaloq.tools.ddk.xtext.export.jvmmodel.ExportJvmModelInferrerTest; +import com.avaloq.tools.ddk.xtext.export.resource.ExportResourceDescriptionManagerTest; import com.avaloq.tools.ddk.xtext.export.scoping.ExportScopingTest; import com.avaloq.tools.ddk.xtext.export.validation.ExportValidationOkTest; import com.avaloq.tools.ddk.xtext.export.validation.ExportValidationTest; @@ -24,6 +25,6 @@ * Empty class serving only as holder for JUnit 5 suite annotations. */ @Suite -@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportValidationOkTest.class, ExportScopingTest.class, ExportJvmModelInferrerTest.class}) +@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportValidationOkTest.class, ExportScopingTest.class, ExportJvmModelInferrerTest.class, ExportResourceDescriptionManagerTest.class}) public class ExportTestSuite { } diff --git a/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF index 2f1495a0a..f60522c69 100644 --- a/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF @@ -38,6 +38,7 @@ Export-Package: com.avaloq.tools.ddk.xtext.export, com.avaloq.tools.ddk.xtext.export.formatting, com.avaloq.tools.ddk.xtext.export.serializer, com.avaloq.tools.ddk.xtext.export.generator, - com.avaloq.tools.ddk.xtext.export.jvmmodel + com.avaloq.tools.ddk.xtext.export.jvmmodel, + com.avaloq.tools.ddk.xtext.export.resource Automatic-Module-Name: com.avaloq.tools.ddk.xtext.export Import-Package: org.apache.log4j diff --git a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java index 3cb93e319..15dcb27d8 100644 --- a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java @@ -13,12 +13,14 @@ import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.generator.IOutputConfigurationProvider; import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.resource.IResourceDescription; import org.eclipse.xtext.xbase.compiler.JvmModelGenerator; import com.avaloq.tools.ddk.xtext.export.conversion.ExportValueConverterService; import com.avaloq.tools.ddk.xtext.export.generator.ExportJvmModelGenerator; import com.avaloq.tools.ddk.xtext.export.generator.ExportOutputConfigurationProvider; import com.avaloq.tools.ddk.xtext.export.naming.ExportQualifiedNameConverter; +import com.avaloq.tools.ddk.xtext.export.resource.ExportResourceDescriptionManager; import com.avaloq.tools.ddk.xtext.formatting.LfLineSeparatorInformation; @@ -70,6 +72,16 @@ public Class bindJvmModelGenerator() { return ExportJvmModelGenerator.class; } + /** + * Binds the description manager which records the Java types the model names as its dependencies. + * + * @return the export specific resource description manager + */ + @Override + public Class bindIResourceDescription$Manager() { // NOPMD + return ExportResourceDescriptionManager.class; + } + @Override public Class bindIFormatter() { return com.avaloq.tools.ddk.xtext.export.formatting.ExportFormatter.class; diff --git a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManager.java b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManager.java new file mode 100644 index 000000000..cd141fc51 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManager.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.export.resource; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.naming.QualifiedName; + +import com.avaloq.tools.ddk.xtext.export.export.ExportModel; +import com.avaloq.tools.ddk.xtext.export.export.Extension; +import com.avaloq.tools.ddk.xtext.expression.resource.AbstractExpressionModelResourceDescriptionManager; +import com.google.inject.Singleton; + + +/** + * Resource description manager for export models, whose {@code extension} classes and expression types are dependencies. + */ +@Singleton +public class ExportResourceDescriptionManager extends AbstractExpressionModelResourceDescriptionManager { + + @Override + protected Set getNamedJavaTypes(final Resource resource) { + if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof ExportModel model)) { + return Set.of(); + } + final Set names = new LinkedHashSet<>(); + for (final Extension declaration : model.getExtensions()) { + if (declaration.getExtension() != null) { + names.add(javaName(List.of(declaration.getExtension().split("::")))); //$NON-NLS-1$ + } + } + addExpressionTypeNames(model, names); + return names; + } +} diff --git a/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF index fcdb8562b..5dcc4ad84 100644 --- a/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF @@ -36,6 +36,7 @@ Export-Package: com.avaloq.tools.ddk.xtext.expression, com.avaloq.tools.ddk.xtext.expression.generator, com.avaloq.tools.ddk.xtext.expression.parser.antlr, com.avaloq.tools.ddk.xtext.expression.parser.antlr.internal, + com.avaloq.tools.ddk.xtext.expression.resource, com.avaloq.tools.ddk.xtext.expression.scoping, com.avaloq.tools.ddk.xtext.expression.serializer, com.avaloq.tools.ddk.xtext.expression.services, diff --git a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/resource/AbstractExpressionModelResourceDescriptionManager.java b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/resource/AbstractExpressionModelResourceDescriptionManager.java new file mode 100644 index 000000000..fd68c6292 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/resource/AbstractExpressionModelResourceDescriptionManager.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.expression.resource; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.xbase.resource.XbaseResourceDescription; +import org.eclipse.xtext.xbase.resource.XbaseResourceDescriptionManager; + +import com.avaloq.tools.ddk.xtext.expression.expression.Identifier; + + +/** + * Resource description manager whose descriptions also import the Java types a model names but only resolves while its Java + * is generated, so that a change to such a type re-queues the model. + */ +public abstract class AbstractExpressionModelResourceDescriptionManager extends XbaseResourceDescriptionManager { + + @Override + protected IResourceDescription createResourceDescription(final Resource resource, final IDefaultResourceDescriptionStrategy strategy) { + return new XbaseResourceDescription(resource, strategy, getCache(), typeResolver, nameConverter) { + private Set importedNames; + + @Override + public Iterable getImportedNames() { + if (importedNames == null) { + final Set names = new LinkedHashSet<>(); + super.getImportedNames().forEach(names::add); + names.addAll(getNamedJavaTypes(resource)); + importedNames = Collections.unmodifiableSet(names); + } + return importedNames; + } + }; + } + + /** + * Returns the names, as created by {@link #javaName(List)}, of the Java types the model in the given resource names. + * + * @param resource + * the resource, must not be {@code null} + * @return the names, never {@code null} + */ + protected abstract Set getNamedJavaTypes(Resource resource); + + /** + * Adds the names of the qualified types named in the expressions below the given element, such as casts and the receivers + * of static calls. + * + * @param root + * the element whose contents are searched, must not be {@code null} + * @param names + * the set to add the names to, must not be {@code null} + */ + protected static void addExpressionTypeNames(final EObject root, final Set names) { + root.eAllContents().forEachRemaining(content -> { + if (content instanceof Identifier identifier && identifier.getId().size() > 1) { + names.add(javaName(identifier.getId())); + } + }); + } + + /** + * Returns the name of a Java type in the form in which changed Java types are reported: dot-separated segments without + * escape characters, in lower case. + * + * @param segments + * the segments of the qualified type name, must not be {@code null} + * @return the name, never {@code null} + */ + protected static QualifiedName javaName(final List segments) { + return QualifiedName.create(segments.stream().map(segment -> segment.startsWith("^") ? segment.substring(1) : segment).toList()).toLowerCase(); //$NON-NLS-1$ + } +} diff --git a/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF index 9c2afb4d9..2fb805d12 100644 --- a/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF @@ -14,6 +14,7 @@ Require-Bundle: com.avaloq.tools.ddk.test.core, com.avaloq.tools.ddk.xtext.scope, com.avaloq.tools.ddk.xtext.test.core, org.eclipse.xtext, + org.eclipse.xtext.common.types, org.mockito.mockito-core, com.avaloq.tools.ddk.xtext.ui, org.eclipse.xtext.xtext.generator, diff --git a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/expression/ScopeResourceDescriptionManagerTest.java b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/expression/ScopeResourceDescriptionManagerTest.java new file mode 100644 index 000000000..0c870d9b3 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/expression/ScopeResourceDescriptionManagerTest.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.generator.expression; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.InternalEObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.common.types.JvmGenericType; +import org.eclipse.xtext.common.types.TypesFactory; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.EObjectDescription; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.IReferenceDescription; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.IResourceFactory; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.resource.impl.AbstractResourceDescription; +import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionDelta; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; +import org.junit.jupiter.api.Test; + +import com.avaloq.tools.ddk.xtext.scope.ScopeStandaloneSetup; +import com.google.inject.Injector; + + +/** + * Tests that the Java types a scope model and the scope models it includes name are imported names of the model, so that a + * change to such a type affects it. + */ +@SuppressWarnings("nls") +public class ScopeResourceDescriptionManagerTest { + + private static final String ECORE_IMPORT = "\n\nimport \"http://www.eclipse.org/emf/2002/Ecore\" as ecore\n\n"; + + private static final String BASE = "scoping test.Base" + ECORE_IMPORT + "extension com::acme::util::BaseExtensions\n\n" + + "inject com.acme.util.Helper as helper\n\n" + "naming {\n ecore::EClass = factory com::acme::util::Names.nameOf(this);\n}\n"; + + private static final String MAIN = "scoping test.Main with test.Base" + ECORE_IMPORT + "extension com::acme::^scope::MainExtensions\n"; + + private static final String PLAIN = "scoping test.Plain" + ECORE_IMPORT; + + private static final String MAIN_EXTENSION_CLASS = "com.acme.scope.MainExtensions"; + + private static final String INCLUDED_EXTENSION_CLASS = "com.acme.util.BaseExtensions"; + + private static final String INJECTED_TYPE = "com.acme.util.Helper"; + + private static final String FACTORY_CLASS = "com.acme.util.Names"; + + private static final String NOT_NAMED = "A type the model does not name must not be an imported name."; + + /** Resources are created with this injector rather than through the global registry, which the shared test runtime uses. */ + private static final Injector INJECTOR = new ScopeStandaloneSetup().createInjector(); + + @Test + public void testOwnAndIncludedDependenciesAreImportedNames() throws IOException { + final IResourceDescription main = describe(loadAll().main()); + assertTrue(imports(main, MAIN_EXTENSION_CLASS), "The model's own extension class must be an imported name."); + assertTrue(imports(main, INCLUDED_EXTENSION_CLASS), "The included model's extension class must be an imported name."); + assertTrue(imports(main, INJECTED_TYPE), "A type injected by an included model must be an imported name."); + assertTrue(imports(main, FACTORY_CLASS), "A factory type named in an included model must be an imported name."); + } + + @Test + public void testChangedTypesAffectModel() throws IOException { + final IResourceDescription main = describe(loadAll().main()); + assertTrue(isAffected(MAIN_EXTENSION_CLASS, main), "A change to the model's own extension class must affect it."); + assertTrue(isAffected(INJECTED_TYPE, main), "A change to a type only an included model names must affect the model."); + } + + @Test + public void testModelWithoutDependenciesIsNotAffected() throws IOException { + final IResourceDescription plain = describe(loadAll().plain()); + assertFalse(imports(plain, MAIN_EXTENSION_CLASS), NOT_NAMED); + assertFalse(imports(plain, FACTORY_CLASS), NOT_NAMED); + assertFalse(isAffected(INCLUDED_EXTENSION_CLASS, plain), "A change to a type named only by other models must not affect the model."); + } + + /** The three models, loaded into one resource set so that names leaking between models would be detected. */ + private record Models(Resource main, Resource plain) { + } + + private static Models loadAll() throws IOException { + final XtextResourceSet resourceSet = INJECTOR.getInstance(XtextResourceSet.class); + resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put("scope", INJECTOR.getInstance(IResourceFactory.class)); + final Resource base = load(resourceSet, "Base", BASE); + final Resource main = load(resourceSet, "Main", MAIN); + final Resource plain = load(resourceSet, "Plain", PLAIN); + EcoreUtil.resolveAll(resourceSet); + for (final Resource resource : List.of(base, main, plain)) { + assertTrue(resource.getErrors().isEmpty(), () -> "The model must parse: " + resource.getURI() + " " + resource.getErrors()); + } + return new Models(main, plain); + } + + private static Resource load(final XtextResourceSet resourceSet, final String name, final String source) throws IOException { + final Resource resource = resourceSet.createResource(URI.createURI("memory:/" + name + ".scope")); + resource.load(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)), null); + return resource; + } + + private static IResourceDescription describe(final Resource resource) { + return INJECTOR.getInstance(IResourceDescription.Manager.class).getResourceDescription(resource); + } + + private static boolean isAffected(final String fqn, final IResourceDescription candidate) { + return INJECTOR.getInstance(IResourceDescription.Manager.class).isAffected(List.of(javaTypeChange(fqn)), candidate, + new ResourceDescriptionsData(List.of(candidate))); + } + + /** + * Returns a change of the given Java type shaped like the ones JDT reports: the resource URI {@code java:/Objects/}, + * exporting one object named by the dot-separated fully qualified name. + */ + private static IResourceDescription.Delta javaTypeChange(final String fqn) { + final URI uri = URI.createURI("java:/Objects/" + fqn); + final JvmGenericType type = TypesFactory.eINSTANCE.createJvmGenericType(); + ((InternalEObject) type).eSetProxyURI(uri.appendFragment(fqn)); + final IEObjectDescription exported = EObjectDescription.create(QualifiedName.create(fqn.split("\\.")), type); + final IResourceDescription changed = new AbstractResourceDescription() { + @Override + protected List computeExportedObjects() { + return List.of(exported); + } + + @Override + public Iterable getImportedNames() { + return List.of(); + } + + @Override + public Iterable getReferenceDescriptions() { + return List.of(); + } + + @Override + public URI getURI() { + return uri; + } + }; + return new DefaultResourceDescriptionDelta(null, changed); + } + + private static boolean imports(final IResourceDescription description, final String fqn) { + final QualifiedName expected = QualifiedName.create(fqn.split("\\.")).toLowerCase(); + for (final QualifiedName name : description.getImportedNames()) { + if (expected.equals(name)) { + return true; + } + } + return false; + } +} diff --git a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java index 64074a5e4..cbaeea987 100644 --- a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java +++ b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java @@ -16,6 +16,7 @@ import com.avaloq.tools.ddk.xtext.generator.expression.ExportExpressionCodeGenerationTest; import com.avaloq.tools.ddk.xtext.generator.expression.ExpressionsExtentionsTest; import com.avaloq.tools.ddk.xtext.generator.expression.ScopeExpressionCodeGenerationTest; +import com.avaloq.tools.ddk.xtext.generator.expression.ScopeResourceDescriptionManagerTest; import com.avaloq.tools.ddk.xtext.generator.test.util.EClassComparatorTest; import com.avaloq.tools.ddk.xtext.generator.test.util.GraphTest; import com.avaloq.tools.ddk.xtext.generator.xbase.test.XbaseGeneratorFragmentTest; @@ -32,6 +33,7 @@ LineEndingDeterminismTest.class, ExpressionsExtentionsTest.class, ScopeExpressionCodeGenerationTest.class, + ScopeResourceDescriptionManagerTest.class, EClassComparatorTest.class, GraphTest.class, XbaseGeneratorFragmentTest.class diff --git a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java index 56b0ddd13..015be9073 100644 --- a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java @@ -16,12 +16,14 @@ import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy; import org.eclipse.xtext.resource.ILocationInFileProvider; +import org.eclipse.xtext.resource.IResourceDescription; import com.avaloq.tools.ddk.xtext.formatting.LfLineSeparatorInformation; import com.avaloq.tools.ddk.xtext.scope.conversion.ScopeValueConverterService; import com.avaloq.tools.ddk.xtext.scope.linking.ScopeLinkingService; import com.avaloq.tools.ddk.xtext.scope.naming.ScopeQualifiedNameConverter; import com.avaloq.tools.ddk.xtext.scope.resource.ScopeLocationInFileProvider; +import com.avaloq.tools.ddk.xtext.scope.resource.ScopeResourceDescriptionManager; import com.avaloq.tools.ddk.xtext.scope.resource.ScopeResourceDescriptionStrategy; @@ -85,6 +87,16 @@ public Class bindIDefaultResource return ScopeResourceDescriptionStrategy.class; } + /** + * Binds the description manager which records the Java types the model names as its dependencies. + * + * @return the scope specific resource description manager + */ + @Override + public Class bindIResourceDescription$Manager() { // NOPMD + return ScopeResourceDescriptionManager.class; + } + @Override public Class bindIFormatter() { return com.avaloq.tools.ddk.xtext.scope.formatting.ScopeFormatter.class; diff --git a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/resource/ScopeResourceDescriptionManager.java b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/resource/ScopeResourceDescriptionManager.java new file mode 100644 index 000000000..7dfc41c37 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/resource/ScopeResourceDescriptionManager.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.scope.resource; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.naming.QualifiedName; + +import com.avaloq.tools.ddk.xtext.expression.resource.AbstractExpressionModelResourceDescriptionManager; +import com.avaloq.tools.ddk.xtext.scope.scope.Extension; +import com.avaloq.tools.ddk.xtext.scope.scope.Injection; +import com.avaloq.tools.ddk.xtext.scope.scope.ScopeModel; +import com.google.inject.Singleton; + + +/** + * Resource description manager for scope models, whose {@code extension} classes, injected types and expression types, and + * those of the models they include, are dependencies. + */ +@Singleton +public class ScopeResourceDescriptionManager extends AbstractExpressionModelResourceDescriptionManager { + + @Override + protected Set getNamedJavaTypes(final Resource resource) { + if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof ScopeModel model)) { + return Set.of(); + } + final Set names = new LinkedHashSet<>(); + addNamedJavaTypes(model, new HashSet<>(), names); + return names; + } + + /** + * Adds the Java types named by the given scope model and, transitively, by the models it includes. + * + * @param model + * the scope model, must not be {@code null} + * @param visited + * the models already visited, which guards against include cycles, must not be {@code null} + * @param names + * the set to add the names to, must not be {@code null} + */ + private static void addNamedJavaTypes(final ScopeModel model, final Set visited, final Set names) { + if (!visited.add(model)) { + return; + } + for (final Extension declaration : model.getExtensions()) { + if (declaration.getExtension() != null) { + names.add(javaName(List.of(declaration.getExtension().split("::")))); //$NON-NLS-1$ + } + } + for (final Injection injection : model.getInjections()) { + if (injection.getType() != null) { + names.add(javaName(List.of(injection.getType().split("\\.")))); //$NON-NLS-1$ + } + } + addExpressionTypeNames(model, names); + for (final ScopeModel included : model.getIncludedScopes()) { + if (!included.eIsProxy()) { + addNamedJavaTypes(included, visited, names); + } + } + } +} From ee533faefe214bbb38f9f2603941dccf726aed73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sat, 26 Sep 2026 14:22:45 +0200 Subject: [PATCH 2/2] test(export): share the grammar-less model parsing of the export tests Move the helper that parses an export model next to an empty grammar resource into ExportTestUtil, so ExportJvmModelInferrerTest and ExportResourceDescriptionManagerTest no longer duplicate it (CPD). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LgVRfQCTxSDwGrN8J5s6fd --- .../jvmmodel/ExportJvmModelInferrerTest.java | 31 ++----------- .../ExportResourceDescriptionManagerTest.java | 27 +---------- .../test/export/util/ExportTestUtil.java | 46 +++++++++++++++++++ 3 files changed, 50 insertions(+), 54 deletions(-) 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 8df23a81a..a541021e5 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 @@ -15,11 +15,9 @@ 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; @@ -27,11 +25,8 @@ 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; @@ -163,35 +158,15 @@ public void accept(final T type, final Procedure1 - * The inferrer resolves the grammar belonging to an export model by loading the {@code .xtext} file next to it. An empty - * resource is registered under that URI so the lookup resolves without demand-loading a grammar; the documentation under - * test does not depend on the grammar. Both resources have to share one resource set, hence the explicit set up here - * instead of the usual test utility call, which creates a new resource set per invocation. + * Parses the header-less export model and installs its derived state; the documentation under test does not depend on the + * grammar, see {@link ExportTestUtil#parseWithoutGrammar(URI, String)}. * * @return the loaded resource, never {@code null} * @throws IOException * if the model cannot be parsed */ private Resource parseHeaderlessModel() throws IOException { - final URI modelUri = getTargetSourceUri(MODEL_NAME + ".export"); - final URI grammarUri = modelUri.trimFileExtension().appendFileExtension("xtext"); - final Resource grammarResource = new ResourceImpl(grammarUri) { - @Override - public boolean isLoaded() { - return true; - } - }; - final XtextResourceSet resourceSet = getXtextTestUtil().getResourceSet(); - resourceSet.getResources().add(grammarResource); - resourceSet.getURIResourceMap().put(grammarUri, grammarResource); - - final XtextResource resource = (XtextResource) resourceSet.createResource(modelUri); - resourceSet.getResources().add(resource); - resource.load(new ByteArrayInputStream(MODEL_SOURCE.getBytes(StandardCharsets.UTF_8)), null); - EcoreUtil.resolveAll(resource); - return resource; + return getXtextTestUtil().parseWithoutGrammar(getTargetSourceUri(MODEL_NAME + ".export"), MODEL_SOURCE); } /** diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java index 6528760d5..b4b5144af 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/resource/ExportResourceDescriptionManagerTest.java @@ -14,16 +14,11 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.InternalEObject; -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.JvmGenericType; import org.eclipse.xtext.common.types.TypesFactory; import org.eclipse.xtext.naming.QualifiedName; @@ -32,7 +27,6 @@ import org.eclipse.xtext.resource.IReferenceDescription; import org.eclipse.xtext.resource.IResourceDescription; import org.eclipse.xtext.resource.XtextResource; -import org.eclipse.xtext.resource.XtextResourceSet; import org.eclipse.xtext.resource.impl.AbstractResourceDescription; import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionDelta; import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; @@ -165,26 +159,7 @@ private static boolean imports(final IResourceDescription description, final Str return false; } - /** - * Parses the given export model with its derived state installed. The inferrer resolves the grammar of an export model by - * loading the {@code .xtext} file next to it, so an empty resource is registered under that URI. - */ private XtextResource parse(final String name, final String source) throws IOException { - final URI modelUri = getTargetSourceUri(name + ".export"); - final URI grammarUri = modelUri.trimFileExtension().appendFileExtension("xtext"); - final Resource grammarResource = new ResourceImpl(grammarUri) { - @Override - public boolean isLoaded() { - return true; - } - }; - final XtextResourceSet resourceSet = getXtextTestUtil().getResourceSet(); - resourceSet.getResources().add(grammarResource); - resourceSet.getURIResourceMap().put(grammarUri, grammarResource); - final XtextResource resource = (XtextResource) resourceSet.createResource(modelUri); - resourceSet.getResources().add(resource); - resource.load(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)), null); - EcoreUtil.resolveAll(resource); - return resource; + return getXtextTestUtil().parseWithoutGrammar(getTargetSourceUri(name + ".export"), source); } } diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java index 260bb5aef..f1d7eacf0 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java @@ -10,6 +10,17 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.test.export.util; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.eclipse.emf.common.util.URI; +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.resource.XtextResource; +import org.eclipse.xtext.resource.XtextResourceSet; + import com.avaloq.tools.ddk.xtext.export.ExportConstants; import com.avaloq.tools.ddk.xtext.export.ui.internal.ExportActivator; import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; @@ -42,4 +53,39 @@ protected Injector getInjector() { return ExportActivator.getInstance().getInjector(ExportConstants.GRAMMAR); } + /** + * Parses an export model from the given source and installs its derived state. + *

+ * The inferrer resolves the grammar belonging to an export model by loading the {@code .xtext} file next to it. An empty + * resource is registered under that URI so the lookup resolves without demand-loading a grammar; the model must therefore + * not depend on its grammar. Both resources share one new resource set. + *

+ * + * @param modelUri + * the URI of the export model, must not be {@code null} + * @param source + * the source of the export model, must not be {@code null} + * @return the loaded resource, never {@code null} + * @throws IOException + * if the model cannot be parsed + */ + public XtextResource parseWithoutGrammar(final URI modelUri, final String source) throws IOException { + final URI grammarUri = modelUri.trimFileExtension().appendFileExtension("xtext"); //$NON-NLS-1$ + final Resource grammarResource = new ResourceImpl(grammarUri) { + @Override + public boolean isLoaded() { + return true; + } + }; + final XtextResourceSet resourceSet = getResourceSet(); + resourceSet.getResources().add(grammarResource); + resourceSet.getURIResourceMap().put(grammarUri, grammarResource); + + final XtextResource resource = (XtextResource) resourceSet.createResource(modelUri); + resourceSet.getResources().add(resource); + resource.load(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)), null); + EcoreUtil.resolveAll(resource); + return resource; + } + }