From d0a7d5fea2023f9f361c81289535fbc9aab3764b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Tue, 3 Mar 2026 21:49:12 +0100 Subject: [PATCH] fix: harden ExportGeneratorX package derivation --- .../generator/ExportGeneratorXTest.export | 14 ++++ .../generator/ExportGeneratorXTest.java | 46 ++++++++++++ .../xtext/test/export/ExportTestSuite.java | 3 +- .../export/generator/ExportGeneratorX.xtend | 73 +++++++++++++++---- 4 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 com.avaloq.tools.ddk.xtext.export.test/resource/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.export create mode 100644 com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.java diff --git a/com.avaloq.tools.ddk.xtext.export.test/resource/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.export b/com.avaloq.tools.ddk.xtext.export.test/resource/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.export new file mode 100644 index 000000000..ee9e0900c --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.export.test/resource/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.export @@ -0,0 +1,14 @@ +import "http://www.avaloq.com/tools/ddk/xtext/export/Export" + +interface { + InterfaceExpression=unordered; + UserData=name; +} + +export InterfaceExpression as ref +{ + data ex = this.getExpr().toString(); +} +export UserData as name +{ +} diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.java new file mode 100644 index 000000000..bdc74a89c --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.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.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.avaloq.tools.ddk.xtext.export.export.ExportModel; +import com.avaloq.tools.ddk.xtext.test.export.util.ExportTestUtil; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; + + +/** + * Regression tests for URI-based package derivation in {@link ExportGeneratorX}. + */ +@SuppressWarnings("nls") +public class ExportGeneratorXTest extends AbstractXtextTest { + + private final ExportGeneratorX exportGeneratorX = getXtextTestUtil().get(ExportGeneratorX.class); + + @Override + protected ExportTestUtil getXtextTestUtil() { + return ExportTestUtil.getInstance(); + } + + @Test + public void testShallowProjectUriFallsBackToProjectPackage() { + final ExportModel model = (ExportModel) getTestSource().getModel(); + + assertEquals("test.naming.ExportGeneratorXTestExportedNamesProvider", exportGeneratorX.getExportedNamesProvider(model)); + assertEquals("test.resource.ExportGeneratorXTestResourceDescriptionManager", exportGeneratorX.getResourceDescriptionManager(model)); + assertEquals("test.resource.ExportGeneratorXTestResourceDescriptionStrategy", exportGeneratorX.getResourceDescriptionStrategy(model)); + assertEquals("test.resource.ExportGeneratorXTestResourceDescriptionConstants", exportGeneratorX.getResourceDescriptionConstants(model)); + assertEquals("test.resource.ExportGeneratorXTestFingerprintComputer", exportGeneratorX.getFingerprintComputer(model)); + assertEquals("test.resource.ExportGeneratorXTestFragmentProvider", exportGeneratorX.getFragmentProvider(model)); + } +} 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 767126c1d..e2711bd6a 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 @@ -13,6 +13,7 @@ import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; +import com.avaloq.tools.ddk.xtext.export.generator.ExportGeneratorXTest; import com.avaloq.tools.ddk.xtext.export.exporting.ExportExportingTest; import com.avaloq.tools.ddk.xtext.export.formatting.ExportFormattingTest; import com.avaloq.tools.ddk.xtext.export.scoping.ExportScopingTest; @@ -23,6 +24,6 @@ * Empty class serving only as holder for JUnit4 annotations. */ @Suite -@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportScopingTest.class, ExportExportingTest.class}) +@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportScopingTest.class, ExportExportingTest.class, ExportGeneratorXTest.class}) public class ExportTestSuite { } diff --git a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorX.xtend b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorX.xtend index 37be4efb9..848db3478 100644 --- a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorX.xtend +++ b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorX.xtend @@ -32,6 +32,10 @@ import org.eclipse.xtext.Grammar class ExportGeneratorX { + static val URI_PROJECT_SEGMENT_INDEX = 1 + static val URI_PACKAGE_START_INDEX = 3 + static val DEFAULT_PACKAGE_SEGMENT = "generated" + @Inject extension Naming @@ -51,15 +55,13 @@ class ExportGeneratorX { } def String getExportedNamesProvider(ExportModel model) { - val uri = model.eResource().getURI(); // TODO this is a hack; to support modularization we should probably add name to export models (as with scope models) - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".naming." + getName(model) + "ExportedNamesProvider"; + return model.basePackage + ".naming." + getName(model) + "ExportedNamesProvider"; } def String getResourceDescriptionManager(ExportModel model) { - val uri = model.eResource().getURI(); // TODO this is a hack; to support modularization we should probably add name to export models (as with scope models) - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".resource." + getName(model) + "ResourceDescriptionManager"; + return model.basePackage + ".resource." + getName(model) + "ResourceDescriptionManager"; } def String getResourceDescriptionManager(Grammar grammar) { @@ -67,33 +69,76 @@ class ExportGeneratorX { } def String getResourceDescriptionStrategy(ExportModel model) { - val uri = model.eResource().getURI(); // TODO this is a hack; to support modularization we should probably add name to export models (as with scope models) - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".resource." + getName(model) + "ResourceDescriptionStrategy"; + return model.basePackage + ".resource." + getName(model) + "ResourceDescriptionStrategy"; } def String getResourceDescriptionConstants(ExportModel model) { - val uri = model.eResource().getURI(); // TODO this is a hack; to support modularization we should probably add name to export models (as with scope models) - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".resource." + getName(model) + "ResourceDescriptionConstants"; + return model.basePackage + ".resource." + getName(model) + "ResourceDescriptionConstants"; } def String getFingerprintComputer(ExportModel model) { - val uri = model.eResource().getURI(); // TODO this is a hack; to support modularization we should probably add name to export models (as with scope models) - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".resource." + getName(model) + "FingerprintComputer"; + return model.basePackage + ".resource." + getName(model) + "FingerprintComputer"; } def String getFragmentProvider(ExportModel model) { - val uri = model.eResource().getURI(); // TODO this is a hack; to support modularization we should probably add name to export models (as with scope models) - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".resource." + getName(model) + "FragmentProvider"; + return model.basePackage + ".resource." + getName(model) + "FragmentProvider"; } def String getExportFeatureExtension(ExportModel model) { - val uri = model.eResource().getURI(); // TODO we still need to add a package to the models. Extension models already have a name in contrast to cases above - return String.join(".", uri.segmentsList().subList(3, uri.segmentCount() - 1)) + ".resource." + model.name + "ExportFeatureExtension"; + return model.basePackage + ".resource." + model.name + "ExportFeatureExtension"; + } + + private def String getBasePackage(ExportModel model) { + val uri = model.eResource.URI + val packageFromUri = uri.packageFromUri + if (packageFromUri !== null) { + return packageFromUri + } + return uri.fallbackPackage + } + + private def String getPackageFromUri(org.eclipse.emf.common.util.URI uri) { + val packageSegments = uri.segmentsList + if (packageSegments.size > URI_PACKAGE_START_INDEX + 1 && "src".equals(packageSegments.get(URI_PROJECT_SEGMENT_INDEX + 1))) { + return String.join(".", packageSegments.subList(URI_PACKAGE_START_INDEX, uri.segmentCount - 1)) + } + return null + } + + private def String getFallbackPackage(org.eclipse.emf.common.util.URI uri) { + val segments = uri.segmentsList + if (segments.size > URI_PROJECT_SEGMENT_INDEX) { + return segments.get(URI_PROJECT_SEGMENT_INDEX).safePackageSegment + } + return DEFAULT_PACKAGE_SEGMENT + } + + private def String getSafePackageSegment(String segment) { + if (segment === null || segment.empty) { + return DEFAULT_PACKAGE_SEGMENT + } + val normalizedSegment = segment.toLowerCase + val builder = new StringBuilder() + for (var i = 0; i < normalizedSegment.length; i++) { + val character = normalizedSegment.charAt(i) + if (builder.length == 0) { + if (Character.isJavaIdentifierStart(character)) { + builder.append(character) + } else if (Character.isJavaIdentifierPart(character)) { + builder.append('_').append(character) + } else { + builder.append('_') + } + } else { + builder.append(if (Character.isJavaIdentifierPart(character)) character else '_') + } + } + return if (builder.length == 0) DEFAULT_PACKAGE_SEGMENT else builder.toString } /**