From a5ddc6273ec5d4447a869248dfa9613b22df7660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sat, 19 Sep 2026 00:38:17 +0200 Subject: [PATCH] fix(export): keep a null export-model name out of the generated documentation The `export for ` header is optional, so `ExportModel.name` is null for every header-less model - which is every model the repository itself carries. The Xtend original appended that name through StringConcatenation, which appends nothing for null; the migrated `String.formatted` call renders it as the literal text "null" instead, so the resource description manager was documented as "Resource description manager for null resources.". Guard the name with `Strings.emptyIfNull`, the same guard the scope inferrer already applies at its two setDocumentation sites. Align ScopeExpressionCompiler.calledFeature with its export twin as well: the Xtend original used `head`, which yields null on an empty list, whereas the migrated `get(0)` throws. The id list of a feature call type is never empty in practice, so this only aligns the behaviour of an unreachable branch. ExportJvmModelInferrerTest covers the documentation: it parses a header-less export model, installs the derived state and asserts the documentation of the inferred resource description manager. The inferrer resolves an export model's grammar from the `.xtext` file next to it, so the test registers an empty resource under that URI; the documentation under test does not depend on the grammar. Both resources must share a resource set, hence the explicit set up instead of the test utility call, which hands out a fresh resource set per invocation. The test bundle gains the xbase and common.types dependencies the JVM model API needs, and a version bump for its changed sources. Co-Authored-By: Claude Fable 5.1 --- .../META-INF/MANIFEST.MF | 6 +- .../pom.xml | 2 +- .../jvmmodel/ExportJvmModelInferrerTest.java | 129 ++++++++++++++++++ .../xtext/test/export/ExportTestSuite.java | 3 +- .../jvmmodel/ExportJvmModelInferrer.java | 2 +- .../jvmmodel/ScopeExpressionCompiler.java | 2 +- 6 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrerTest.java diff --git a/com.avaloq.tools.ddk.xtext.export.test/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.export.test/META-INF/MANIFEST.MF index 0f316887ec..5fd91faa39 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.export.test/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext.export.test Bundle-SymbolicName: com.avaloq.tools.ddk.xtext.export.test;singleton:=true -Bundle-Version: 17.3.1.qualifier +Bundle-Version: 17.3.2.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy @@ -13,6 +13,8 @@ Require-Bundle: com.avaloq.tools.ddk.xtext.export, com.avaloq.tools.ddk.xtext, junit-jupiter-api, junit-jupiter-engine, - junit-platform-suite-api + junit-platform-suite-api, + org.eclipse.xtext.xbase, + org.eclipse.xtext.common.types Export-Package: com.avaloq.tools.ddk.xtext.test.export Automatic-Module-Name: com.avaloq.tools.ddk.xtext.export.test diff --git a/com.avaloq.tools.ddk.xtext.export.test/pom.xml b/com.avaloq.tools.ddk.xtext.export.test/pom.xml index 15935f52c9..8574ecefc0 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/pom.xml +++ b/com.avaloq.tools.ddk.xtext.export.test/pom.xml @@ -6,7 +6,7 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.1-SNAPSHOT + 17.3.2-SNAPSHOT com.avaloq.tools.ddk.xtext.export.test eclipse-test-plugin 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 new file mode 100644 index 0000000000..899b3e701f --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/jvmmodel/ExportJvmModelInferrerTest.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * 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.jvmmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +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.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.JvmGenericType; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder; +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 the JVM model inferred for an export model without an {@code export <name> for <grammar>} header. + *

+ * 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"}. + */ +@SuppressWarnings("nls") +public class ExportJvmModelInferrerTest extends AbstractXtextTest { + + /** Name of the header-less export model parsed by this test. */ + private static final String MODEL_NAME = "HeaderlessExportModel"; + + /** Source of the header-less export model parsed by this test. */ + private static final String MODEL_SOURCE = "import \"http://www.eclipse.org/emf/2002/Ecore\" as ecore\n" + + "\n" + + "export ecore::EClass as name\n"; + + /** Simple name suffix of the inferred resource description manager. */ + private static final String MANAGER_SUFFIX = "ResourceDescriptionManager"; + + /** Documentation expected on the inferred resource description manager; the name is empty, hence the two blanks. */ + private static final String EXPECTED_DOCUMENTATION = "Resource description manager for resources."; + + @Override + protected ExportTestUtil getXtextTestUtil() { + return ExportTestUtil.getInstance(); + } + + /** + * This test builds its source in memory and has no test source file. {@inheritDoc} + */ + @Override + protected String getTestSourceFileName() { + return null; + } + + @Test + public void testResourceDescriptionManagerDocumentationOfHeaderlessModel() throws IOException { + final Resource resource = parseHeaderlessModel(); + final JvmGenericType manager = findInferredManager(resource); + assertNotNull(manager, "The inferrer must infer a resource description manager for a header-less export model."); + final JvmTypesBuilder typesBuilder = getXtextTestUtil().get(JvmTypesBuilder.class); + assertEquals(EXPECTED_DOCUMENTATION, typesBuilder.getDocumentation(manager), + "A null export model name must render as an empty string, not as \"null\"."); + } + + /** + * Parses the header-less export model 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 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. + * + * @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; + } + + /** + * Returns the inferred resource description manager of the given resource. + * + * @param resource + * the resource of the export model, must not be {@code null} + * @return the inferred type, or {@code null} if the inferrer did not produce one + */ + private JvmGenericType findInferredManager(final Resource resource) { + for (final EObject content : resource.getContents()) { + if (content instanceof JvmGenericType && ((JvmGenericType) content).getSimpleName().endsWith(MANAGER_SUFFIX)) { + return (JvmGenericType) content; + } + } + return null; + } + +} 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 938d4fa16e..92cf96062e 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 @@ -14,6 +14,7 @@ import org.junit.platform.suite.api.Suite; 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.scoping.ExportScopingTest; import com.avaloq.tools.ddk.xtext.export.validation.ExportValidationOkTest; import com.avaloq.tools.ddk.xtext.export.validation.ExportValidationTest; @@ -23,6 +24,6 @@ * Empty class serving only as holder for JUnit 5 suite annotations. */ @Suite -@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportValidationOkTest.class, ExportScopingTest.class}) +@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportValidationOkTest.class, ExportScopingTest.class, ExportJvmModelInferrerTest.class}) public class ExportTestSuite { } 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 608e4638b8..879d3dbfb6 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 @@ -308,7 +308,7 @@ private void inferResourceDescriptionManager(final ExportModel model, final IJvm it.getSuperTypes().add(_typeReferenceBuilder.typeRef(AbstractCachingResourceDescriptionManager.class)); it.getAnnotations().add(typeOnlyAnnotation(Singleton.class)); addSuppressWarningsAll(it); - jvmTypesBuilder.setDocumentation(it, "Resource description manager for %s resources.".formatted(model.getName())); + jvmTypesBuilder.setDocumentation(it, "Resource description manager for %s resources.".formatted(Strings.emptyIfNull(model.getName()))); final Procedure1 fieldInitializer = (final JvmField field) -> { field.setVisibility(JvmVisibility.PUBLIC); field.setStatic(true); diff --git a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/jvmmodel/ScopeExpressionCompiler.java b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/jvmmodel/ScopeExpressionCompiler.java index ae1eb69ef8..6fb8011bbf 100644 --- a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/jvmmodel/ScopeExpressionCompiler.java +++ b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/jvmmodel/ScopeExpressionCompiler.java @@ -608,7 +608,7 @@ private boolean _isThis(final Identifier it) { } private String calledFeature(final FeatureCall it) { - return it.getType().getId().get(0); + return it.getType().getId().isEmpty() ? null : it.getType().getId().get(0); } private String serialize(final EObject it) {