diff --git a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/VirtualContainerTest.java b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/VirtualContainerTest.java new file mode 100644 index 000000000..913067b1b --- /dev/null +++ b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/VirtualContainerTest.java @@ -0,0 +1,597 @@ +/** + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Obeo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Eclipse Public License, version 2, as published by + * the Eclipse Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Eclipse Public License for more details. + * + * You should have received a copy of the Eclipse Public License + * along with this program. If not, see . + * + * @license EPL-2.0 + */ +package org.omg.sysml.interactive.tests; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.InternalEObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.xtext.EcoreUtil2; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; +import org.omg.sysml.interactive.SysMLInteractive; +import org.omg.sysml.lang.sysml.BindingConnector; +import org.omg.sysml.lang.sysml.Classifier; +import org.omg.sysml.lang.sysml.ConnectionDefinition; +import org.omg.sysml.lang.sysml.Element; +import org.omg.sysml.lang.sysml.Feature; +import org.omg.sysml.lang.sysml.FeatureChainExpression; +import org.omg.sysml.lang.sysml.FeatureMembership; +import org.omg.sysml.lang.sysml.ReferenceUsage; +import org.omg.sysml.lang.sysml.SuccessionAsUsage; +import org.omg.sysml.lang.sysml.SysMLFactory; +import org.omg.sysml.lang.sysml.TransitionUsage; +import org.omg.sysml.lang.sysml.Type; +import org.omg.sysml.util.ElementUtil; +import org.omg.sysml.util.FeatureUtil; +import org.omg.sysml.util.SysMLLibraryUtil; +import org.omg.sysml.util.TypeUtil; +import org.omg.sysml.util.UsageUtil; +import org.omg.sysml.util.VirtualContainer; + +/** + * Tests that the elements the implementation creates outside the model for implied relationships + * (feature chains and binding connectors) are linked to the model element for which they are created, + * and that {@link ElementUtil#getEffectiveContainer(EObject)} reaches the model from them. + */ +public class VirtualContainerTest { + + private static ResourceSet resourceSet; + private final List models = new ArrayList<>(); + + /** Loads and resolves libraries once. */ + @BeforeClass + public static void loadLibraries() { + SysMLInteractive interactive = SysMLInteractive.createInstance(); + interactive.setVerbose(false); + interactive.getLibraryIndexCache().setIndexDisabled(true); + interactive.loadLibrary(Path.of(System.getProperty("libraryPath")).toAbsolutePath().toString()); + resourceSet = interactive.getResourceSet(); + for (int i = 0; i < resourceSet.getResources().size(); i++) { + EcoreUtil2.resolveLazyCrossReferences(resourceSet.getResources().get(i), null); + } + } + + /** Removes each scenario without altering the shared library contents. */ + @After + public void removeModels() { + for (Resource resource : models) { + resource.unload(); + resourceSet.getResources().remove(resource); + } + } + + /** + * The result of a {@code FeatureChainExpression} subsets a chain of the source parameter and the + * source target feature. The expression adds that chain to the implicit general types of its result + * when it is transformed; the chain is created detached and its virtual container, and therefore + * its effective container, must be the expression result. + */ + @Test + public void featureChainExpressionResultChainIsLinkedToTheResult() throws Exception { + // package Chains { + // feature a { feature b; } + // feature c = a.b; + // } + Resource resource = parse("chainExpression.kerml", """ + package Chains { + feature a { feature b; } + feature c = a.b; + } + """); + Feature c = findByName(resource, "c", Feature.class); + FeatureChainExpression expression = (FeatureChainExpression)FeatureUtil.getValuationFor(c).getValue(); + Feature result = expression.getResult(); + + // Transforming the expression alone records the chain "source.b" as an implicit general type of + // the result, without materializing it: the chain is still detached. + ElementUtil.transform(expression); + Feature chain = findDetachedChain(TypeUtil.getImplicitGeneralTypesFor(result)); + assertSame(result, VirtualContainer.getVirtualContainer(chain)); + assertSame(result, ElementUtil.getEffectiveContainer(chain)); + } + + /** + * A feature bound to a value without an explicit specialization subsets the chain of the value + * expression and its result. That chain is created detached and must be linked to the valued + * feature. + */ + @Test + public void boundValueChainIsLinkedToTheValuedFeature() throws Exception { + // package BoundValue { + // feature x; + // feature y = x; + // } + Resource resource = parse("boundValue.kerml", """ + package BoundValue { + feature x; + feature y = x; + } + """); + Feature y = findByName(resource, "y", Feature.class); + + // The implicit subsetting of y targets the detached chain "value.result". + Feature chain = findDetachedChain(TypeUtil.getImplicitGeneralTypesFor(y)); + assertSame(y, VirtualContainer.getVirtualContainer(chain)); + assertSame(y, ElementUtil.getEffectiveContainer(chain)); + } + + /** + * A succession whose source is a decision node, and a succession whose target is a merge node, + * specialize a detached chain through the control node. Each chain must be linked to its + * succession. + */ + @Test + public void decisionAndMergeChainsAreLinkedToTheSuccession() throws Exception { + // action def Control { + // action A1; + // action A2; + // decide D; + // if true then A1; + // else A2; + // action B1; + // then M; + // action B2; + // then M; + // merge M; + // } + Resource resource = parse("control.sysml", """ + action def Control { + action A1; + action A2; + decide D; + if true then A1; + else A2; + action B1; + then M; + action B2; + then M; + merge M; + } + """); + + // Outgoing succession of D: its chained general is "D.outgoingHBLink". + SuccessionAsUsage fromDecision = findSuccession(resource, "D", true); + Feature decisionChain = findDetachedChain(TypeUtil.getImplicitGeneralTypesFor(fromDecision)); + assertSame(fromDecision, VirtualContainer.getVirtualContainer(decisionChain)); + assertSame(fromDecision, ElementUtil.getEffectiveContainer(decisionChain)); + + // Incoming succession of M: its chained general is "M.incomingHBLink". + SuccessionAsUsage toMerge = findSuccession(resource, "M", false); + Feature mergeChain = findDetachedChain(TypeUtil.getImplicitGeneralTypesFor(toMerge)); + assertSame(toMerge, VirtualContainer.getVirtualContainer(mergeChain)); + assertSame(toMerge, ElementUtil.getEffectiveContainer(mergeChain)); + } + + /** + * The payload parameter of a transition with an accepter subsets the chain of the accepter and its + * payload parameter. That chain is created detached and must be linked to the payload parameter. + */ + @Test + public void transitionPayloadChainIsLinkedToThePayloadParameter() throws Exception { + // state def Machine { + // attribute def Signal; + // entry; then s1; + // state s1; + // state s2; + // transition t first s1 accept sig : Signal then s2; + // } + Resource resource = parse("transition.sysml", """ + state def Machine { + attribute def Signal; + entry; then s1; + state s1; + state s2; + transition t first s1 accept sig : Signal then s2; + } + """); + TransitionUsage transition = findByName(resource, "t", TransitionUsage.class); + ReferenceUsage payload = (ReferenceUsage)UsageUtil.getPayloadParameterOf(transition); + assertNotNull("Missing transition payload parameter", payload); + + // The implicit subsetting of the payload parameter targets the detached chain "accepter.sig". + Feature chain = findDetachedChain(TypeUtil.getImplicitGeneralTypesFor(payload)); + assertSame(payload, VirtualContainer.getVirtualContainer(chain)); + assertSame(payload, ElementUtil.getEffectiveContainer(chain)); + } + + /** + * The transformation of a valued feature creates a binding connector detached, linked to the + * feature, then inserts it into the model. Once inserted, the connector keeps its virtual + * container but its effective container is its real container. + */ + @Test + public void valueBindingConnectorKeepsItsVirtualContainerOnceContained() throws Exception { + // package Binding { + // feature x; + // feature y = x; + // } + Resource resource = parse("binding.kerml", """ + package Binding { + feature x; + feature y = x; + } + """); + Feature y = findByName(resource, "y", Feature.class); + + // Transformation creates the binding connector of y's value and adds it to the model. The value + // expression also owns a binding connector for its own result, linked to the expression, so the + // connector is selected by its virtual container. + ElementUtil.transformAll(resource, true); + BindingConnector connector = findBindingConnectorLinkedTo(y); + assertNotNull("Expected the value binding connector in the model", connector.eContainer()); + assertSame(y, VirtualContainer.getVirtualContainer(connector)); + assertSame(connector.eContainer(), ElementUtil.getEffectiveContainer(connector)); + } + + /** + * A detached chain resolves library types from the model through its virtual container, as the + * element it was created for does. The rules applied by the chain's own adapter then find its + * library general: without the link, the lookup returns nothing and the chain gets no general. + */ + @Test + public void detachedChainResolvesLibraryTypesFromItsModel() throws Exception { + // package Library { + // feature x; + // feature y = x; + // } + Resource resource = parse("library.kerml", """ + package Library { + feature x; + feature y = x; + } + """); + Feature y = findByName(resource, "y", Feature.class); + Feature chain = findDetachedChain(TypeUtil.getImplicitGeneralTypesFor(y)); + + // The chain has no resource: the library is found through its virtual container, y. + Type things = SysMLLibraryUtil.getLibraryType(y, "Base::things"); + assertNotNull(things); + assertSame(things, SysMLLibraryUtil.getLibraryType(chain, "Base::things")); + + // The default rule of the chain's own adapter therefore finds its library general + // (checkFeatureSpecialization). + List generals = TypeUtil.getImplicitGeneralTypesFor(chain).stream().map(Type::getQualifiedName).toList(); + assertTrue("Expected Base::things in " + generals, generals.contains("Base::things")); + } + + /** + * A binding connector created for a valued feature, before it is inserted into the model, + * resolves library types from the model through its virtual container. + */ + @Test + public void detachedBindingConnectorResolvesLibraryTypesFromItsModel() throws Exception { + // package Connector { + // feature x; + // feature y = x; + // } + Resource resource = parse("connector.kerml", """ + package Connector { + feature x; + feature y = x; + } + """); + Feature y = findByName(resource, "y", Feature.class); + + // Transformation without implicit elements creates the value binding connector of y but does + // not insert it: the connector stays detached, linked to y. + ElementUtil.transformAll(resource, false); + List connectors = new ArrayList<>(); + TypeUtil.forEachImplicitBindingConnectorOf(y, (connector, membershipKind) -> connectors.add(connector)); + BindingConnector connector = connectors.stream() + .filter(candidate -> VirtualContainer.getVirtualContainer(candidate) == y).findFirst().orElseThrow(); + assertNull(connector.eResource()); + + // The library general of a binding connector is found from the connector itself. + assertNotNull(SysMLLibraryUtil.getLibraryType(connector, "Links::selfLinks")); + } + + /** + * A Type that is the root of its resource and the featuring type of its ends stays the root of + * its resource after transformation: materializing the implied TypeFeaturing of an end must not + * move the root into that relationship, which would create a containment cycle and leave the + * resource. + */ + @Test + public void resourceRootStaysInItsResourceAfterTransformation() throws Exception { + // A Type cannot be the root of a parsed resource, whose root is a Namespace: the model is built + // programmatically, a ConnectionDefinition with two end Features as the only resource content. + Resource resource = resourceSet.createResource(URI.createURI("memory:/root.sysml")); + models.add(resource); + ConnectionDefinition connectionDefinition = SysMLFactory.eINSTANCE.createConnectionDefinition(); + resource.getContents().add(connectionDefinition); + for (int i = 0; i < 2; i++) { + Feature end = SysMLFactory.eINSTANCE.createFeature(); + end.setIsEnd(true); + FeatureMembership membership = SysMLFactory.eINSTANCE.createFeatureMembership(); + membership.setOwnedMemberFeature(end); + connectionDefinition.getOwnedRelationship().add(membership); + } + + // The ends are featured by the ConnectionDefinition; inserting that TypeFeaturing must leave + // the ConnectionDefinition where it is. + ElementUtil.transformAll(connectionDefinition, true); + assertNull(connectionDefinition.eContainer()); + assertSame(resource, connectionDefinition.eResource()); + } + + /** + * The featuring type of a variable feature is a {@code _snapshots} feature created + * outside the model and stored as an implicit featuring type of the feature. Storing it records + * the feature, which owns the implied TypeFeaturing, as its virtual container. + */ + @Test + public void snapshotsFeaturingTypeIsLinkedToTheVariableFeature() throws Exception { + Resource resource = parse("snapshots.kerml", """ + package Snapshots { + class C { + var feature x; + } + } + """); + Feature x = findByName(resource, "x", Feature.class); + + // Transforming x computes its featuring type (checkFeatureFeatureMembershipTypeFeaturing): + // a new C_snapshots feature redefining Occurrence::snapshots, which is not inserted. + ElementUtil.transform(x); + Feature snapshots = x.getFeaturingType().stream() + .filter(Feature.class::isInstance).map(Feature.class::cast) + .filter(featuring -> "C_snapshots".equals(featuring.getDeclaredName())) + .findFirst().orElseThrow(); + assertNull(snapshots.eContainer()); + + // Expected: its virtual container is x, through which it reaches the model. + assertSame(x, VirtualContainer.getVirtualContainer(snapshots)); + assertSame(x, ElementUtil.getClosestElementInResource(snapshots)); + } + + /** + * The binding connector of an initial value is featured by a {@code that.startShot} chain + * created outside the model. The chain is stored as an implicit featuring type of the connector, + * so the connector is its virtual container, and the chain reaches the model through the + * connector's own virtual container, the valued feature. + */ + @Test + public void initialValueFeaturingChainIsLinkedToTheBindingConnector() throws Exception { + // package Initial { + // class C { + // feature x; + // var feature y := x; + // } + // } + Resource resource = parse("initial.kerml", """ + package Initial { + class C { + feature x; + var feature y := x; + } + } + """); + Feature y = findByName(resource, "y", Feature.class); + + // Transforming y creates its value binding connector (checkFeatureValueBindingConnector), + // featured by the chain that.startShot because the value is initial. + ElementUtil.transform(y); + BindingConnector connector = findImplicitBindingConnectorOf(y); + Feature chain = connector.getFeaturingType().stream() + .filter(Feature.class::isInstance).map(Feature.class::cast) + .filter(featuring -> featuring.getChainingFeature().size() == 2) + .findFirst().orElseThrow(); + + // Expected: the chain is linked to the connector, and the connector to y. + assertSame(connector, VirtualContainer.getVirtualContainer(chain)); + assertSame(y, VirtualContainer.getVirtualContainer(connector)); + assertSame(y, ElementUtil.getClosestElementInResource(chain)); + } + + /** + * An unresolved proxy has no container or resource but is not a detached inferred element. + * Attaching it must leave it without a virtual container or access to the model's library, + * so its placeholder metaclass cannot acquire library specializations. + */ + @Test + public void unresolvedProxyHasNoVirtualContainer() throws Exception { + Resource resource = parse("proxyContext.kerml", """ + package Context { + feature x; + } + """); + Feature x = findByName(resource, "x", Feature.class); + + // A proxy's concrete class is a placeholder, not the type of the unresolved referent. + Feature proxy = SysMLFactory.eINSTANCE.createAcceptActionUsage(); + ((InternalEObject)proxy).eSetProxyURI(URI.createURI("memory:/unresolved.sysml#referent")); + VirtualContainer.attach(proxy, x); + + // The proxy must not reach x's resource and infer Actions::acceptActions from its class. + assertTrue(proxy.eIsProxy()); + assertNull(VirtualContainer.getVirtualContainer(proxy)); + assertNull(ElementUtil.getClosestElementInResource(proxy)); + assertNull(SysMLLibraryUtil.getLibraryType(proxy, "Actions::acceptActions")); + } + + /** + * In a model built without a resource, a root can be stored as the target of an implied + * relationship owned by one of its own members, for example as the featuring type of a nested + * feature. Linking the root to that member would close a cycle of effective containers: the + * link is refused, and the walk to the model terminates. + */ + @Test + public void rootIsNotLinkedToOneOfItsMembers() { + // classifier Root { feature member; }, built without a resource. + Classifier root = SysMLFactory.eINSTANCE.createClassifier(); + Feature member = SysMLFactory.eINSTANCE.createFeature(); + TypeUtil.addOwnedFeatureTo(root, member); + + // Storing root as an implied relationship target of member must not link them. + VirtualContainer.attach(root, member); + assertNull(VirtualContainer.getVirtualContainer(root)); + + // Expected: no element in a resource is found, instead of an endless walk. + assertNull(ElementUtil.getClosestElementInResource(member)); + } + + /** + * An element parsed from text is created in the model: it has no virtual container and its + * effective container is its real container. + */ + @Test + public void parsedElementHasNoVirtualContainer() throws Exception { + // package Parsed { + // feature x; + // } + Resource resource = parse("parsed.kerml", """ + package Parsed { + feature x; + } + """); + Feature x = findByName(resource, "x", Feature.class); + + assertNull(VirtualContainer.getVirtualContainer(x)); + assertSame(x.eContainer(), ElementUtil.getEffectiveContainer(x)); + } + + /** + * Parses a model into a new in-memory resource of the shared resource set and registers it for + * removal after the test. + * + * @param name the resource name, whose extension selects the language + * @param text the textual model + * @return the loaded resource, checked to have no parsing errors + * @throws Exception when the text cannot be loaded + */ + private Resource parse(String name, String text) throws Exception { + Resource resource = resourceSet.createResource(URI.createURI("memory:/" + name)); + models.add(resource); + resource.load(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), Map.of()); + assertTrue(resource.getErrors().toString(), resource.getErrors().isEmpty()); + return resource; + } + + /** + * Returns the first element of a resource with the given declared name and metaclass. + * + * @param resource the resource searched, in containment order + * @param name the declared name + * @param kind the expected metaclass + * @return the element found + * @throws AssertionError when no such element exists + */ + private static T findByName(Resource resource, String name, Class kind) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + EObject object = contents.next(); + if (kind.isInstance(object) && name.equals(((Element)object).getDeclaredName())) { + return kind.cast(object); + } + } + throw new AssertionError("Missing " + kind.getSimpleName() + " " + name); + } + + /** + * Returns the first feature chain that is not contained in the model among the given general + * types. + * + * @param generals the general types searched, in order + * @return the detached feature chain + * @throws AssertionError when the list contains no detached feature chain + */ + private static Feature findDetachedChain(List generals) { + for (Type general : generals) { + if (general instanceof Feature feature && !feature.getOwnedFeatureChaining().isEmpty() + && feature.eContainer() == null) { + return feature; + } + } + throw new AssertionError("Missing detached feature chain among " + generals); + } + + /** + * Returns the first SuccessionAsUsage of a resource whose source or target end is the named node. + * + * @param resource the resource searched, in containment order + * @param nodeName the declared name of the end feature + * @param fromNode {@code true} to match the source end, {@code false} to match the target end + * @return the succession found + * @throws AssertionError when no such succession exists + */ + private static SuccessionAsUsage findSuccession(Resource resource, String nodeName, boolean fromNode) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + if (contents.next() instanceof SuccessionAsUsage succession) { + Feature end; + if (fromNode) { + end = UsageUtil.getSourceOf(succession); + } else { + end = UsageUtil.getTargetOf(succession); + } + if (end != null && nodeName.equals(end.getDeclaredName())) { + return succession; + } + } + } + throw new AssertionError("Missing succession for " + nodeName); + } + + /** + * Returns the first BindingConnector contained in a feature whose virtual container is that + * feature. + * + * @param feature the feature whose contents are searched + * @return the binding connector found + * @throws AssertionError when no such binding connector exists + */ + private static BindingConnector findBindingConnectorLinkedTo(Feature feature) { + for (var contents = feature.eAllContents(); contents.hasNext();) { + if (contents.next() instanceof BindingConnector connector + && VirtualContainer.getVirtualContainer(connector) == feature) { + return connector; + } + } + throw new AssertionError("Missing binding connector linked to " + feature.getDeclaredName()); + } + + /** + * Returns the first implicit BindingConnector of a feature, not yet inserted into the model. + * + * @param feature the feature whose implicit binding connectors are searched + * @return the binding connector found + * @throws AssertionError when the feature has no implicit binding connector + */ + private static BindingConnector findImplicitBindingConnectorOf(Feature feature) { + List connectors = new ArrayList<>(); + TypeUtil.forEachImplicitBindingConnectorOf(feature, (connector, membershipKind) -> connectors.add(connector)); + if (connectors.isEmpty()) { + throw new AssertionError("Missing implicit binding connector of " + feature.getDeclaredName()); + } + return connectors.get(0); + } +} diff --git a/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar b/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar index b50622a96..532495458 100644 Binary files a/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar and b/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar differ diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureAdapter.java index ab97dd536..875f8d63e 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureAdapter.java @@ -59,12 +59,12 @@ import org.omg.sysml.lang.sysml.Type; import org.omg.sysml.lang.sysml.TypeFeaturing; import org.omg.sysml.lang.sysml.VisibilityKind; -import org.omg.sysml.util.ConnectorUtil; import org.omg.sysml.util.ElementUtil; import org.omg.sysml.util.ExpressionUtil; import org.omg.sysml.util.FeatureUtil; import org.omg.sysml.util.NonNotifyingEObjectEList; import org.omg.sysml.util.TypeUtil; +import org.omg.sysml.util.VirtualContainer; public class FeatureAdapter extends TypeAdapter { @@ -126,10 +126,13 @@ public void addImplicitFeaturingTypes() { public void addFeaturingType(Type type) { implicitFeaturingTypes.add(type); + VirtualContainer.attach(type, getTarget()); } public void addFeaturingTypes(Collection featuringTypes) { - implicitFeaturingTypes.addAll(featuringTypes); + for (Type featuringType : featuringTypes) { + addFeaturingType(featuringType); + } } public void forEachImplicitFeaturingType(Consumer action) { @@ -811,8 +814,7 @@ protected void addImplicitFeaturingTypesIfNecessary() { } protected BindingConnector addBindingConnector(Collection featuringTypes, Feature source, Feature target) { - BindingConnector connector = ConnectorUtil.createBindingConnector(source, target); - ConnectorUtil.transformBindingConnector(connector, getTarget()); + BindingConnector connector = createImplicitBindingConnector(source, target); addImplicitMemberBindingConnector(connector); FeatureUtil.addFeaturingTypesTo(connector, featuringTypes); return connector; diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/TypeAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/TypeAdapter.java index 8501a829f..a167fafec 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/TypeAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/TypeAdapter.java @@ -1,6 +1,7 @@ /******************************************************************************* * SysML 2 Pilot Implementation * Copyright (c) 2021-2026 Model Driven Solutions, Inc. + * Copyright (c) 2026 Obeo * * This program is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License as published by @@ -60,6 +61,7 @@ import org.omg.sysml.util.ImplicitGeneralizationMap; import org.omg.sysml.util.NonNotifyingEObjectEList; import org.omg.sysml.util.TypeUtil; +import org.omg.sysml.util.VirtualContainer; public class TypeAdapter extends NamespaceAdapter { @@ -364,6 +366,7 @@ protected static boolean hasNoConformingSpecializations(Type type, Class kind public void addImplicitGeneralType(EClass eClass, Type general) { if (isAddImplicitGeneralTypes && general != null && general != getTarget() && !isImplicitSpecializationFor(eClass, general)) { implicitGeneralTypes.computeIfAbsent(eClass, e -> new ArrayList<>()).add(general); + VirtualContainer.attach(general, getTarget()); } } @@ -514,10 +517,21 @@ protected static Feature getBaseTypeFeature(Element element) { // Transformation + /** + * Creates and transforms a binding connector implied by a constraint on the target Type. The + * implied relationship is the owning membership of the connector in the target, so the target + * is its virtual container. It is recorded before the transformation, which looks up the + * library general of the connector, and before any query on the connector. + */ + protected BindingConnector createImplicitBindingConnector(Feature source, Feature target) { + BindingConnector connector = VirtualContainer.attach(ConnectorUtil.createBindingConnector(source, target), getTarget()); + ConnectorUtil.transformBindingConnector(connector); + return connector; + } + public BindingConnector addBindingConnector(Feature source, Feature target) { Type type = getTarget(); - BindingConnector connector = ConnectorUtil.createBindingConnector(source, target); - ConnectorUtil.transformBindingConnector(connector, getTarget()); + BindingConnector connector = createImplicitBindingConnector(source, target); Type contextType = ConnectorUtil.getContextTypeFor(connector); if (contextType == type) { addImplicitFeatureBindingConnector(connector); diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ConnectorUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ConnectorUtil.java index f0e70b470..d6b5d7d09 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ConnectorUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ConnectorUtil.java @@ -3,6 +3,7 @@ * Copyright (c) 2021-2022 Model Driven Solutions, Inc. * Copyright (c) 2023 Mgnite Inc. * Copyright (c) 2026 Budapest University of Technology and Economics + * Copyright (c) 2026 Obeo * * This program is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License as published by @@ -53,9 +54,9 @@ public static BindingConnector createBindingConnector(Feature source, Feature ta return connector; } - public static void transformBindingConnector(BindingConnector connector, Type owner) { + public static void transformBindingConnector(BindingConnector connector) { TypeUtil.addImplicitGeneralTypeTo(connector, SysMLPackage.eINSTANCE.getSubsetting(), - SysMLLibraryUtil.getLibraryType(owner, ImplicitGeneralizationMap.getDefaultSupertypeFor(connector.getClass(), "binary"))); + SysMLLibraryUtil.getLibraryType(connector, ImplicitGeneralizationMap.getDefaultSupertypeFor(connector.getClass(), "binary"))); for (Feature end: TypeUtil.getEndFeatureOf(connector)) { ElementUtil.transform(end); } diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ElementUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ElementUtil.java index be418e844..4131a08d5 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ElementUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ElementUtil.java @@ -120,7 +120,47 @@ public static String escapeName(String name) { public static boolean isIdentifier(String name) { return name.matches("[a-zA-Z_]\\w*"); } - + + // Containment + + /** + * Returns the container of an element: its real container when it has one, otherwise the + * virtual container recorded for an element created outside the model (see + * {@link VirtualContainer}). + * + * @param element the element whose container is requested + * @return the real or virtual container, or {@code null} for a root or an unlinked element + */ + public static EObject getEffectiveContainer(EObject element) { + if (element == null) { + return null; + } + EObject container = element.eContainer(); + if (container != null) { + return container; + } + return VirtualContainer.getVirtualContainer(element); + } + + /** + * Returns the element itself when it belongs to a resource, otherwise its closest effective + * container (see {@link #getEffectiveContainer(EObject)}) that does. The walk terminates as + * long as effective containers form no cycle: {@link VirtualContainer#attach} never records a + * link that would close one, and no element must later be inserted into an element that + * reaches it through effective containers. + * + * @param element the element from which the model is reached + * @return the closest element that belongs to a resource, or {@code null} when neither the + * element nor any of its effective containers does + */ + public static EObject getClosestElementInResource(EObject element) { + EObject current = element; + while (current != null && current.eResource() == null) { + current = getEffectiveContainer(current); + } + return current; + } + // Qualified Names /** diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/FeatureUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/FeatureUtil.java index c9f858fe9..3a38018e3 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/FeatureUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/FeatureUtil.java @@ -2,6 +2,7 @@ * SysML 2 Pilot Implementation * Copyright (c) 2021-2025 Model Driven Solutions, Inc. * Copyright (c) 2026 Budapest University of Technology and Economics + * Copyright (c) 2026 Obeo * * This program is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License as published by @@ -350,7 +351,18 @@ public static void insertImplicitTypeFeaturings(Feature feature) { featuring.setIsImplied(true); featuring.setFeaturingType(type); featuring.setFeatureOfType(feature); - if (type.getOwningRelationship() == null && !type.eIsProxy()) { + // A featuring type created for this implied TypeFeaturing, such as an _snapshots + // feature or a feature chain, is detached: it becomes an owned related element of the + // inserted relationship. Having no owning relationship does not prove that a type is + // detached: the root namespace of a model has no owner either. A parsed model cannot cause + // a confusion, since its root namespace is an unnamed Namespace, which cannot be a featuring + // type. A model built by other means, such as a test building elements programmatically, + // can however have a Type as the root of its resource and as the featuring type of one of its + // own members. Adopting that root would move it out of its resource into its own member, a + // containment cycle that ElementUtil.getClosestElementInResource could not leave. Only a type + // that is neither owned nor in a resource is therefore adopted; the root of a model built + // without any resource still cannot be told apart from a detached type. + if (!type.eIsProxy() && type.getOwningRelationship() == null && type.eResource() == null) { featuring.getOwnedRelatedElement().add(type); } feature.getOwnedRelationship().add(featuring); diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/SysMLLibraryUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/SysMLLibraryUtil.java index 6e7bbd26d..f46389897 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/SysMLLibraryUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/SysMLLibraryUtil.java @@ -158,9 +158,12 @@ public static void setProviderLookup(ProviderLookup lookup) { * Resolves a library element by qualified name relative to the given context * element. * - *

The context element is used to locate the originating resource and obtain - * the appropriate {@link IModelLibraryProvider}. If no provider is available - * or the provider cannot resolve the name, this method returns {@code null}. + *

The lookup starts from the closest element in a resource: the context itself, or, for an + * element created outside the model, the closest of its effective containers that belongs to + * a resource (see {@link ElementUtil#getClosestElementInResource}). That element locates the + * originating resource, obtains the appropriate {@link IModelLibraryProvider} and is passed to + * it. If no provider is available or the provider cannot resolve the name, this method returns + * {@code null}. * * @param context the lookup context * @param name the qualified library name to resolve @@ -168,9 +171,17 @@ public static void setProviderLookup(ProviderLookup lookup) { * resolved */ public static Element getLibraryElement(Element context, String name) { - Resource resource = context == null ? null : context.eResource(); + Element lookupContext = context; + Resource resource = null; + if (ElementUtil.getClosestElementInResource(context) instanceof Element attached) { + lookupContext = attached; + resource = attached.eResource(); + } IModelLibraryProvider provider = getInstance(resource); - return provider == null ? null : provider.getElement(context, name); + if (provider == null) { + return null; + } + return provider.getElement(lookupContext, name); } /** diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/TypeUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/TypeUtil.java index ba23ba562..5af7bc9cb 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/TypeUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/TypeUtil.java @@ -478,7 +478,9 @@ public static void insertImplicitSpecializations(Type type) { newSpecialization.setIsImplied(true); newSpecialization.setGeneral(general); newSpecialization.setSpecific(type); - if (general.getOwningRelationship() == null && !general.eIsProxy()) { + // Only a detached general, such as a feature chain, is adopted; a root Type of its + // resource stays where it is (see FeatureUtil.insertImplicitTypeFeaturings). + if (!general.eIsProxy() && general.getOwningRelationship() == null && general.eResource() == null) { newSpecialization.getOwnedRelatedElement().add(general); } type.getOwnedRelationship().add(newSpecialization); diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/VirtualContainer.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/VirtualContainer.java new file mode 100644 index 000000000..1fb32628b --- /dev/null +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/VirtualContainer.java @@ -0,0 +1,107 @@ +/** + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Obeo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Eclipse Public License, version 2, as published by + * the Eclipse Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Eclipse Public License for more details. + * + * You should have received a copy of the Eclipse Public License + * along with this program. If not, see . + * + * @license EPL-2.0 + */ +package org.omg.sysml.util; + +import org.eclipse.emf.common.notify.impl.AdapterImpl; +import org.eclipse.emf.ecore.EObject; +import org.omg.sysml.lang.sysml.Element; + +/** + * Links an element created outside the model, such as a feature chain or a binding connector + * inferred for an implied relationship, to the model element for which it was created. + *

+ * The specification has no detached elements: the implied Relationship is owned by the element + * being constrained, and a feature chain it targets is owned by that Relationship. KerML §8.4.3.1: + * "In all cases, the source and owningRelatedElement of the Relationship is the Element being + * constrained, with the target being as given in the last column of the table." KerML §7.3.4.6: + * "the related element specified using the feature chain notation becomes an owned related feature + * of the relationship with the feature chain as notated." The implementation creates such elements + * before the Relationship that will own them; this adapter records their future owner so that + * scope lookups can reach the model from them. It is recorded when such an element is stored as + * the target of an implied relationship: an implicit general type, an implicit featuring type or + * an implicit binding connector, whose implied relationship is its owning membership. + *

+ * The link does not change the model. Once the element is really contained, its real container + * takes precedence: use {@link ElementUtil#getEffectiveContainer(EObject)} rather than this class. + *

+ * Specification quotations are from KerML 1.1 Beta 2. + */ +public final class VirtualContainer extends AdapterImpl { + + private final Element container; + + private VirtualContainer(Element container) { + this.container = container; + } + + /** + * Records {@code container} as the virtual container of an element that is the root of a + * detached tree: it is not a proxy, has no container and is not in a resource. Any other element + * is left unchanged, as is an element that already has a virtual container. + *

+ * A model built without a resource has roots that are not in a resource either, and one of them + * can be stored as the target of an implied relationship owned by one of its own members, such + * as the featuring type of a nested feature. The link is not recorded when {@code element} is + * already an effective container of {@code container}, so that effective containers never form + * a cycle and {@link ElementUtil#getClosestElementInResource(EObject)} terminates. + * + * @param the type of the element + * @param element the element stored as the target of an implied relationship + * @param container the model element that owns the implied relationship + * @return {@code element} + */ + public static T attach(T element, Element container) { + if (isDetachedRoot(element) && container != null && getVirtualContainer(element) == null + && !isEffectiveContainerOf(element, container)) { + element.eAdapters().add(new VirtualContainer(container)); + } + return element; + } + + private static boolean isDetachedRoot(EObject element) { + return element != null && !element.eIsProxy() && element.eContainer() == null && element.eResource() == null; + } + + private static boolean isEffectiveContainerOf(EObject ancestor, EObject element) { + for (EObject current = element; current != null; current = ElementUtil.getEffectiveContainer(current)) { + if (current == ancestor) { + return true; + } + } + return false; + } + + /** + * Returns the virtual container recorded for an element. + * + * @param element the element to inspect + * @return the recorded container, or {@code null} when none was recorded + */ + public static Element getVirtualContainer(EObject element) { + if (element == null) { + return null; + } + for (var adapter : element.eAdapters()) { + if (adapter instanceof VirtualContainer virtual) { + return virtual.container; + } + } + return null; + } +} diff --git a/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar b/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar index ba7d63795..0ddf06956 100644 Binary files a/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar and b/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar differ