From c1cd2192d93fe9f2bb51135b7d31652b3fb5f8c3 Mon Sep 17 00:00:00 2001 From: Axel RICHARD Date: Tue, 22 Sep 2026 01:14:38 +0000 Subject: [PATCH] [2527] Export chained connector ends and the interface keyword Models created before the importer set referencedFeature for owned chains only store the chain as ownedRelatedElement of the ReferenceSubsetting; resolve the referenced feature from there as a fallback. InterfaceUsage must be exported with the interface keyword, which is mandatory in the grammar. Bug: https://github.com/eclipse-syson/syson/issues/2527 Signed-off-by: Axel RICHARD --- CHANGELOG.adoc | 1 + .../application/export/ImportExportTests.java | 28 +++++++++++ .../textual/SysMLElementSerializerTest.java | 47 +++++++++++++++++-- .../textual/SysMLElementSerializer.java | 34 ++++++++++++-- .../pages/release-notes/2026.11.0.adoc | 2 + 5 files changed, 103 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 3feb8f3f06..50a2973f5e 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -34,6 +34,7 @@ The `getCommentBody`, `getComment`, and `getDocumentation` query APIs are now pr - https://github.com/eclipse-syson/syson/issues/1074[#1074] [diagrams] Fix available diagram tools on standard diagrams created on a root namespace - https://github.com/eclipse-syson/syson/issues/2510[#2510] [import] Report syntax errors that may leave an uploaded SysML file partially imported. The parsed elements are still imported, and the loading report includes the parser diagnostics and a warning about possibly missing elements. +- https://github.com/eclipse-syson/syson/issues/2527[#2527] [export] Export connector ends whose feature chain is only owned by the `ReferenceSubsetting`, and export `InterfaceUsage` with the `interface` keyword instead of `connection`. === Improvements diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java index 06f53516ce..e2bac5bb20 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/export/ImportExportTests.java @@ -1494,6 +1494,34 @@ public void checkConnectionUsageWithBody() throws IOException { .check(); } + @Test + @DisplayName("GIVEN a model with an InterfaceUsage whose end is a feature chain, WHEN importing and exporting the model, THEN the InterfaceUsage should be exported properly") + public void checkInterfaceUsageWithChainedEnd() throws IOException { + var input = """ + package P { + port def PI; + part def PD { port p : PI; } + part a { part b : PD; } + part c : PD; + interface i connect a.b.p to c.p; + }"""; + var expected = """ + package P { + port def PI; + part def PD { + port p : PI; + } + part a { + part b : PD; + } + part c : PD; + interface i connect a.b.p to c.p; + }"""; + this.checker.textToImport(input) + .expectedResult(expected) + .check(); + } + /** * Test import/export on test file StateTest.sysml. The content of StateTest.sysml that have been copied below * is under LGPL-3.0-only license. The LGPL-3.0-only license is accessible at the root of this repository, in the diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java index eb00e27c97..7bb15ba533 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java @@ -142,6 +142,8 @@ public class SysMLElementSerializerTest { private static final String BODY = "A body"; + private static final String SOCKET = "socket"; + private ModelBuilder builder; private List status; @@ -1650,7 +1652,7 @@ public void interfaceUsageWithPortEnds() { PartUsage rootPart = this.builder.createWithName(PartUsage.class, "part1"); PartUsage heater = this.builder.createInWithName(PartUsage.class, rootPart, "heater"); - PortUsage socket = this.builder.createInWithName(PortUsage.class, heater, "socket"); + PortUsage socket = this.builder.createInWithName(PortUsage.class, heater, SOCKET); PortUsage outlet = this.builder.createInWithName(PortUsage.class, rootPart, "outlet"); Feature source = this.builder.createFeatureChaining(heater, socket); @@ -1659,7 +1661,7 @@ public void interfaceUsageWithPortEnds() { interfaceUsage.getOwnedRelationship().add(this.createConnectionEndFeatureMembership(source)); interfaceUsage.getOwnedRelationship().add(this.createConnectionEndFeatureMembership(outlet)); - this.assertTextualFormEquals("connection interface1 connect heater.socket to outlet;", interfaceUsage); + this.assertTextualFormEquals("interface interface1 connect heater.socket to outlet;", interfaceUsage); } @Test @@ -1677,7 +1679,42 @@ public void interfaceUsageWithNestedPortEnd() { interfaceUsage.getOwnedRelationship().add(this.createConnectionEndFeatureMembership(source)); interfaceUsage.getOwnedRelationship().add(this.createConnectionEndFeatureMembership(outlet)); - this.assertTextualFormEquals("connection interface1 connect livingRoom.heater.socket to outlet;", interfaceUsage); + this.assertTextualFormEquals("interface interface1 connect livingRoom.heater.socket to outlet;", interfaceUsage); + } + + @Test + public void interfaceUsageWithChainedEndsWithoutReferencedFeature() { + PartUsage rootPart = this.builder.createWithName(PartUsage.class, "root"); + + PartUsage livingRoom = this.builder.createInWithName(PartUsage.class, rootPart, "livingRoom"); + PartUsage heater = this.builder.createInWithName(PartUsage.class, livingRoom, "heater"); + PortUsage socket = this.builder.createInWithName(PortUsage.class, heater, SOCKET); + PortUsage outlet = this.builder.createInWithName(PortUsage.class, rootPart, "outlet"); + + Feature source = this.builder.createFeatureChaining(livingRoom, heater, socket); + InterfaceUsage interfaceUsage = this.builder.createWithName(InterfaceUsage.class, "interface1"); + this.addAsFeatureMember(rootPart, interfaceUsage); + interfaceUsage.getOwnedRelationship().add(this.createChainedEndFeatureMembership(source)); + interfaceUsage.getOwnedRelationship().add(this.createConnectionEndFeatureMembership(outlet)); + + this.assertTextualFormEquals("interface interface1 connect livingRoom.heater.socket to outlet;", interfaceUsage); + } + + /** + * Creates an end whose feature chain is only owned by the {@link ReferenceSubsetting}, without + * {@code referencedFeature} set, as produced by old models and imports. + */ + private EndFeatureMembership createChainedEndFeatureMembership(Feature chain) { + EndFeatureMembership endFeatureMembership = this.fact.createEndFeatureMembership(); + PortUsage endFeature = this.fact.createPortUsage(); + endFeature.setIsEnd(true); + endFeatureMembership.getOwnedRelatedElement().add(endFeature); + endFeatureMembership.setMemberElement(endFeature); + ReferenceSubsetting refSubsetting = this.fact.createReferenceSubsetting(); + refSubsetting.setSubsettingFeature(endFeature); + refSubsetting.getOwnedRelatedElement().add(chain); + endFeature.getOwnedRelationship().add(refSubsetting); + return endFeatureMembership; } @Test @@ -1697,7 +1734,7 @@ public void connectionUsageWithMetadataOnItsEnds() { interfaceUsage.getOwnedRelationship().add(sourceEnd); interfaceUsage.getOwnedRelationship().add(targetEnd); - this.assertTextualFormEquals("connection interface1 connect #original socket to #derive outlet;", interfaceUsage); + this.assertTextualFormEquals("interface interface1 connect #original socket to #derive outlet;", interfaceUsage); } /** @@ -1735,7 +1772,7 @@ public void interfaceUsageWithUnresolvedPortEnd() { interfaceUsage.getOwnedRelationship().add(this.createConnectionEndFeatureMembership(socket)); interfaceUsage.getOwnedRelationship().add(this.createUnresolvedConnectionEndFeatureMembership()); - this.assertTextualFormEquals("connection interface1 connect socket to ;", interfaceUsage); + this.assertTextualFormEquals("interface interface1 connect socket to ;", interfaceUsage); } @Test diff --git a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/textual/SysMLElementSerializer.java b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/textual/SysMLElementSerializer.java index a01513524f..3a7acfb79a 100644 --- a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/textual/SysMLElementSerializer.java +++ b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/textual/SysMLElementSerializer.java @@ -77,6 +77,7 @@ import org.eclipse.syson.sysml.Import; import org.eclipse.syson.sysml.IncludeUseCaseUsage; import org.eclipse.syson.sysml.InterfaceDefinition; +import org.eclipse.syson.sysml.InterfaceUsage; import org.eclipse.syson.sysml.InvocationExpression; import org.eclipse.syson.sysml.ItemDefinition; import org.eclipse.syson.sysml.ItemUsage; @@ -421,9 +422,14 @@ public String caseConnectionUsage(ConnectionUsage connectionUsage) { .filter(e -> !ends.contains(e)) .toList(); - if (!declarationAndValueBuilder.isEmpty() || !contentMemberships.isEmpty()) { - // If the ConnectionUsage as a declaration, a value or some content we need to use the keyword connection - builder.appendWithSpaceIfNeeded("connection").appendWithSpaceIfNeeded(declarationAndValueBuilder); + if (connectionUsage instanceof InterfaceUsage || !declarationAndValueBuilder.isEmpty() || !contentMemberships.isEmpty()) { + // InterfaceUsage always requires its keyword; ConnectionUsage only when it has a declaration, a value or some content + if (connectionUsage instanceof InterfaceUsage) { + builder.appendWithSpaceIfNeeded(this.getUsageKeyword(connectionUsage)); + } else { + builder.appendWithSpaceIfNeeded(SysMLv2Keywords.CONNECTION); + } + builder.appendWithSpaceIfNeeded(declarationAndValueBuilder); } if (!connectorPartBuilder.isEmpty()) { @@ -1776,7 +1782,7 @@ private void appendConnectorEnd(Appender builder, Feature endFeature) { } private void appendOwnedReferenceSubsetting(Appender builder, ReferenceSubsetting refSubsetting) { - Feature referencedFeature = refSubsetting.getReferencedFeature(); + Feature referencedFeature = this.resolveReferencedFeature(refSubsetting); if (referencedFeature != null) { @@ -1792,6 +1798,26 @@ private void appendOwnedReferenceSubsetting(Appender builder, ReferenceSubsettin } } + /** + * Resolves the referenced feature, falling back to an owned feature chain when no reference is set. + * + * @param refSubsetting + * the reference subsetting to resolve + * @return the referenced feature or an owned feature chain, or {@code null} if neither is available + */ + private Feature resolveReferencedFeature(ReferenceSubsetting refSubsetting) { + Feature referencedFeature = refSubsetting.getReferencedFeature(); + if (referencedFeature == null) { + referencedFeature = refSubsetting.getOwnedRelatedElement().stream() + .filter(Feature.class::isInstance) + .map(Feature.class::cast) + .filter(feature -> !feature.eIsProxy() && !feature.getOwnedFeatureChaining().isEmpty()) + .findFirst() + .orElse(null); + } + return referencedFeature; + } + private void appendOwnedCrossMultiplicityMember(Appender builder, OwningMembership owningMembership) { if (owningMembership.getOwnedMemberElement() instanceof Feature ownedFeature) { this.appendMultiplicityPart(builder, ownedFeature); diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.11.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.11.0.adoc index fe143dfee0..d064018334 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.11.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.11.0.adoc @@ -29,6 +29,8 @@ Use the existing content tools to display these elements. Existing representatio ** Report syntax errors that may leave an uploaded SysML file partially imported. The parsed elements are still imported, and the loading report identifies the syntax problem and warns about possibly missing elements. +** Export connector ends that reference feature chains, such as `battery.powerPort to batmobileEngine.enginePort`, instead of leaving the ends empty. +** `InterfaceUsage` elements are now exported with the `interface` keyword. == Improvements