From 793edf06a5dc1ba5b4f6e82774c26d7645013df1 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 1/2] fix: derive a valid package for shallow export model URIs in ExportGeneratorX ExportGeneratorX.getPrefix derives the target package from raw URI segment arithmetic and misbehaves on every short URI shape: 1 segment (e.g. ParseHelper's __synthetic0.export) crashes with IndexOutOfBoundsException (subList(-1, 0)); 2 segments emit the literal segment "resource"; project-root URIs emit the raw project name (uncompilable generated code when the project is not named like a Java package); source-folder-root URIs emit an empty package, producing leading-dot qualified names. Keep the derivation for URIs with five or more segments byte for byte (segments between position 3 and the file) and make every shorter shape fall back to the project name when it already is a valid Java package name (SourceVersion.isName) or "generated" otherwise. No existing test ever failed on this: the builder participant catches the exception per resource and logs it (the long-standing "Error during compilation of '....export'" noise in master test runs), and export tests never assert generated output. ExportGeneratorXTest pins the full shape table with synthetic resource URIs. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018ANGM7L3BaEGfGUmPVKRt4 --- .../generator/ExportGeneratorXTest.java | 73 +++++++++++++++++++ .../xtext/test/export/ExportTestSuite.java | 3 +- .../export/generator/ExportGeneratorX.xtend | 13 +++- 3 files changed, 84 insertions(+), 5 deletions(-) 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/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..37cabe8c4 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/export/generator/ExportGeneratorXTest.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.impl.ResourceImpl; +import org.junit.jupiter.api.Test; + +import com.avaloq.tools.ddk.xtext.export.export.ExportFactory; +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 static final String DERIVED_PACKAGE_PROVIDER = "com.avaloq.naming.fooExportedNamesProvider"; + private static final String PROJECT_FALLBACK_PROVIDER = "MyProject.naming.fooExportedNamesProvider"; + private static final String DEFAULT_FALLBACK_PROVIDER = "generated.naming.fooExportedNamesProvider"; + + private final ExportGeneratorX exportGeneratorX = getXtextTestUtil().get(ExportGeneratorX.class); + + @Override + protected ExportTestUtil getXtextTestUtil() { + return ExportTestUtil.getInstance(); + } + + @Override + protected String getTestSourceFileName() { + return null; // all tests fabricate synthetic resource URIs + } + + @Test + public void testNestedSourceFolderUrisKeepDerivedPackage() { + assertEquals(DERIVED_PACKAGE_PROVIDER, exportedNamesProviderFor("platform:/resource/MyProject/src/com/avaloq/foo.export")); + assertEquals(DERIVED_PACKAGE_PROVIDER, exportedNamesProviderFor("platform:/resource/MyProject/src-gen/com/avaloq/foo.export")); + assertEquals("main.java.com.avaloq.naming.fooExportedNamesProvider", exportedNamesProviderFor("platform:/resource/MyProject/src/main/java/com/avaloq/foo.export")); + } + + @Test + public void testShortUrisFallBackToValidPackage() { + assertEquals(PROJECT_FALLBACK_PROVIDER, exportedNamesProviderFor("platform:/resource/MyProject/foo.export")); + assertEquals(PROJECT_FALLBACK_PROVIDER, exportedNamesProviderFor("platform:/resource/MyProject/src/foo.export")); + assertEquals(DEFAULT_FALLBACK_PROVIDER, exportedNamesProviderFor("platform:/resource/1My-Project/foo.export")); + } + + @Test + public void testSingleSegmentUriFallsBackToDefaultPackage() { + assertEquals(DEFAULT_FALLBACK_PROVIDER, exportedNamesProviderFor("foo.export")); + } + + private String exportedNamesProviderFor(final String uri) { + final Resource resource = new ResourceImpl(URI.createURI(uri)); + final ExportModel model = ExportFactory.eINSTANCE.createExportModel(); + resource.getContents().add(model); + return exportGeneratorX.getExportedNamesProvider(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 ea8a86330..a7de215fc 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; @@ -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, ExportExportingTest.class}) +@SelectClasses({ExportFormattingTest.class, ExportValidationTest.class, ExportValidationOkTest.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 23325a04a..601cae9a5 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 @@ -24,6 +24,7 @@ import com.google.inject.Inject import java.util.Collection import java.util.List import java.util.Map +import javax.lang.model.SourceVersion import org.eclipse.emf.ecore.EAttribute import org.eclipse.emf.ecore.EClass import org.eclipse.emf.ecore.EClassifier @@ -53,11 +54,15 @@ class ExportGeneratorX { def List getPrefix(URI uri) { // TODO we still need to add a package to the models. Extension models already have a name in contrast to cases above - if (uri.segmentsList().size > 3) { - return uri.segmentsList().subList(3, uri.segmentCount() - 1); - } else { - return uri.segmentsList().subList(uri.segmentCount() - 2, uri.segmentCount() - 1); + val segments = uri.segmentsList + if (segments.size > 4) { + return segments.subList(3, segments.size - 1) } + // shorter shapes have no package segments: fall back to a valid package name + if (segments.size > 2 && SourceVersion.isName(segments.get(1))) { + return #[segments.get(1)] + } + return #['generated'] } def String getExportedNamesProvider(ExportModel model) { From 733e78c7783e065fe90f4ba953e45a85d9ebfdfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sat, 11 Jul 2026 10:27:02 +0200 Subject: [PATCH 2/2] fix: tolerate an unresolvable grammar in export model generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sibling-grammar lookup in ExportGeneratorX.getGrammar demand-loads .xtext next to the export model; for models with no such sibling (every test .export file, models built via ParseHelper) the load throws and the builder participant logs "Error during compilation" and silently aborts generation for the file — dozens of ERROR entries in every full test run, and the generator's main path never executes over the test fixtures. Make grammar resolution defensive: guard a missing resource or resource set, reject unresolved targetGrammar proxies, and catch the demand-load failure, returning null. Downstream, GeneratorUtil.allInstantiatedTypes treats a null grammar as "no parser-instantiated subtypes" (covering typeMap and canContain), and the remaining generator call sites already tolerate a null grammar (grammar?.name in ExportedNamesProviderGenerator; an explicit null check in ResourceDescriptionManagerGenerator). Export model generation now degrades gracefully instead of aborting. Phase 0 of #1458. --- .../generator/ExportGeneratorXTest.java | 26 +++++++++++++++++++ .../export/generator/ExportGeneratorX.xtend | 17 +++++++++--- .../expression/generator/GeneratorUtil.java | 4 ++- 3 files changed, 42 insertions(+), 5 deletions(-) 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 index 37cabe8c4..01d90afb2 100644 --- 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 @@ -11,10 +11,15 @@ package com.avaloq.tools.ddk.xtext.export.generator; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; 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.resource.impl.ResourceSetImpl; +import org.eclipse.xtext.Grammar; +import org.eclipse.xtext.XtextFactory; import org.junit.jupiter.api.Test; import com.avaloq.tools.ddk.xtext.export.export.ExportFactory; @@ -32,6 +37,7 @@ public class ExportGeneratorXTest extends AbstractXtextTest { private static final String DERIVED_PACKAGE_PROVIDER = "com.avaloq.naming.fooExportedNamesProvider"; private static final String PROJECT_FALLBACK_PROVIDER = "MyProject.naming.fooExportedNamesProvider"; private static final String DEFAULT_FALLBACK_PROVIDER = "generated.naming.fooExportedNamesProvider"; + private static final String NO_SIBLING_URI = "platform:/resource/TEST/NoSibling.export"; private final ExportGeneratorX exportGeneratorX = getXtextTestUtil().get(ExportGeneratorX.class); @@ -64,6 +70,26 @@ public void testSingleSegmentUriFallsBackToDefaultPackage() { assertEquals(DEFAULT_FALLBACK_PROVIDER, exportedNamesProviderFor("foo.export")); } + @Test + public void testGetGrammarReturnsNullWithoutResolvableGrammar() { + final ExportModel detached = ExportFactory.eINSTANCE.createExportModel(); + assertNull(exportGeneratorX.getGrammar(detached), "model without a resource"); + + final ExportModel withoutResourceSet = ExportFactory.eINSTANCE.createExportModel(); + new ResourceImpl(URI.createURI(NO_SIBLING_URI)).getContents().add(withoutResourceSet); + assertNull(exportGeneratorX.getGrammar(withoutResourceSet), "resource without a resource set"); + + final ExportModel withoutSibling = ExportFactory.eINSTANCE.createExportModel(); + final Resource resource = new ResourceSetImpl().createResource(URI.createURI(NO_SIBLING_URI)); + resource.getContents().add(withoutSibling); + assertNull(exportGeneratorX.getGrammar(withoutSibling), "missing sibling grammar must not throw"); + + final Grammar proxy = XtextFactory.eINSTANCE.createGrammar(); + ((InternalEObject) proxy).eSetProxyURI(URI.createURI("platform:/resource/TEST/Unresolved.xtext#/")); + withoutSibling.setTargetGrammar(proxy); + assertNull(exportGeneratorX.getGrammar(withoutSibling), "unresolved targetGrammar proxy"); + } + private String exportedNamesProviderFor(final String uri) { final Resource resource = new ResourceImpl(URI.createURI(uri)); final ExportModel model = ExportFactory.eINSTANCE.createExportModel(); 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 601cae9a5..82cf5e6f9 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 @@ -43,13 +43,22 @@ class ExportGeneratorX { } def Grammar getGrammar(ExportModel model) { - val uri = model.eResource.URI // Grammar should be set correctly for export extensions, not yet for normal export sources - if(model.targetGrammar !== null) { + if (model.targetGrammar !== null && !model.targetGrammar.eIsProxy) { return model.targetGrammar; } - val grammarResource = model.eResource.resourceSet.getResource(uri.trimSegments(1).appendSegment(uri.trimFileExtension.lastSegment + '.xtext'), true) - return grammarResource?.contents.head as Grammar + val resource = model.eResource + if (resource?.resourceSet === null) { + return null + } + val uri = resource.URI + try { + val grammarResource = resource.resourceSet.getResource(uri.trimSegments(1).appendSegment(uri.trimFileExtension.lastSegment + '.xtext'), true) + return grammarResource?.contents.head as Grammar + } catch (RuntimeException e) { + // no resolvable sibling grammar; all generators tolerate a null grammar + return null + } } def List getPrefix(URI uri) { diff --git a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorUtil.java b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorUtil.java index 0a32edf62..c0e0a73cd 100644 --- a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorUtil.java +++ b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/generator/GeneratorUtil.java @@ -55,7 +55,9 @@ protected GeneratorUtil() { */ public static Set allInstantiatedTypes(final Grammar grammar) { Set result = Sets.newLinkedHashSet(); - collectInstantiatedTypes(grammar, result); + if (grammar != null) { // no grammar -> no parser-instantiated subtypes + collectInstantiatedTypes(grammar, result); + } return result; }