Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,18 @@
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;
Expand Down Expand Up @@ -163,35 +158,15 @@ public <T extends JvmDeclaredType> void accept(final T type, final Procedure1<?
}

/**
* Parses the header-less export model and installs its derived state.
* <p>
* 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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*******************************************************************************
* 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.IOException;
import java.util.List;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.InternalEObject;
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.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/<FQN>},
* 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<IEObjectDescription> computeExportedObjects() {
return List.of(exported);
}

@Override
public Iterable<QualifiedName> getImportedNames() {
return List.of();
}

@Override
public Iterable<IReferenceDescription> 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;
}

private XtextResource parse(final String name, final String source) throws IOException {
return getXtextTestUtil().parseWithoutGrammar(getTargetSourceUri(name + ".export"), source);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
* <p>
* 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.
* </p>
*
* @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;
}

}
3 changes: 2 additions & 1 deletion com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down Expand Up @@ -70,6 +72,16 @@ public Class<? extends JvmModelGenerator> 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<? extends IResourceDescription.Manager> bindIResourceDescription$Manager() { // NOPMD
return ExportResourceDescriptionManager.class;
}

@Override
public Class<? extends org.eclipse.xtext.formatting.IFormatter> bindIFormatter() {
return com.avaloq.tools.ddk.xtext.export.formatting.ExportFormatter.class;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<QualifiedName> getNamedJavaTypes(final Resource resource) {
if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof ExportModel model)) {
return Set.of();
}
final Set<QualifiedName> 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;
}
}
1 change: 1 addition & 0 deletions com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading