From 8fa8e80254ab243eb8e9cca86b71ba970f8ee540 Mon Sep 17 00:00:00 2001 From: atheate Date: Mon, 11 May 2026 12:05:56 +0200 Subject: [PATCH 1/2] Fix #224: Containment pattern optimized to support reference --- CLAUDE.md | 3 + .../AutoGenExtensions/ElementExtensions.cs | 219 +++++++ ...lCorePocoValidationGeneratorTestFixture.cs | 69 +++ .../SysML2.NET.CodeGenerator.Tests.csproj | 4 + .../UmlCorePocoValidationGenerator.cs | 544 ++++++++++++++++++ .../SysML2.NET.CodeGenerator.csproj | 3 + .../Uml/core-poco-validation-template.hbs | 115 ++++ .../Extend/FeatureExtensionsTestFixture.cs | 88 +-- .../Extend/NamespaceExtensionsTestFixture.cs | 8 +- .../Extend/PackageExtensionsTestFixture.cs | 5 +- .../ElementExtensionsTestFixture.cs | 242 +++++++- .../AutoGenExtensions/ElementExtensions.cs | 181 ++++++ SysML2.NET/Extensions/ElementExtensions.cs | 216 ++++++- TESTING.md | 248 ++++++++ 14 files changed, 1866 insertions(+), 79 deletions(-) create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs create mode 100644 SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs create mode 100644 SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs create mode 100644 SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs create mode 100644 TESTING.md diff --git a/CLAUDE.md b/CLAUDE.md index 2bb52015..72bcc74e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,8 @@ dotnet-coverage collect "dotnet test SysML2.NET.sln --no-build" -f xml -o covera Test framework: **NUnit**. Test classes use `[TestFixture]` and `[Test]` attributes. +**When writing or modifying unit tests** in any `*.Tests/` project: read `TESTING.md` at the repo root for the NUnit conventions (one `[Test]` per method-under-test, `Assert.That` everywhere, `Assert.EnterMultipleScope` only for consecutive asserts, mandatory positive + negative coverage, assertion idiom preferences, `Verify{MethodUnderTest}` naming). + ## Architecture ### Code Generation @@ -155,3 +157,4 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package - Use meaningful variable names instead of single-letter names in any context (e.g., 'charIndex' instead of 'i', 'currentChar' instead of 'c', 'element' instead of 'e') - Use 'NotSupportedException' (not 'NotImplementedException') for placeholder/stub methods that require manual implementation - Prefer C# property patterns ('x is IType { Prop: value }') over declared-variable-plus-predicate form ('x is IType name && name.Prop == value') when the narrowed variable is only consulted once; the property-pattern form is more concise and intent-revealing +- Surround every braced block (`if`, `else if`, `while`, `for`, `foreach`, `switch`, `using`, `try`/`catch`/`finally`, `lock`, `do…while`, anonymous `{ }`) with a blank line on both sides — the rule does NOT apply at the very start/end of a method body, nor between a `}` and a continuation keyword (`else`, `catch`, `finally`, `while` of `do…while`) that belongs to the same control flow diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs b/SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs new file mode 100644 index 00000000..45bb0e93 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs @@ -0,0 +1,219 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Extensions +{ + using SysML2.NET.Core.POCO.Root.Elements; + + /// + /// Auto-generated companion to the hand-coded partial class. + /// + public static partial class ElementExtensions + { + /// + /// Determines whether is a structurally valid element to be added as an + /// of , based on + /// the typed composite property declared by the bridge's metaclass in the KerML/SysML2 metamodel. + /// + /// The bridge . + /// The candidate . + /// true if 's runtime type satisfies the constraint; + /// otherwise false. + public static bool QueryIsValidForContainment(this IRelationship bridgeRelationship, IElement target) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage, + SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage, + SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage, + SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage, + SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage, + SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage, + SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => target is SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage, + SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => target is SysML2.NET.Core.POCO.Systems.Actions.IActionUsage, + SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => target is SysML2.NET.Core.POCO.Kernel.Behaviors.IStep, + SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => target is SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage, + SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage, + SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => target is not null, + SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => target is SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement, + SysML2.NET.Core.POCO.Root.Elements.IRelationship => target is not null, + _ => true, + }; + } + + /// + /// Returns the simple C# interface name of the expected target type for the supplied + /// , used to compose the error message thrown by + /// AssignOwnership when the target is not a valid containment target. + /// + /// The bridge . + /// The expected target type's simple interface name. + public static string ExpectedContainmentTypeName(this IRelationship bridgeRelationship) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage), + SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage), + SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage), + SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage), + SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage), + SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage), + SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => nameof(SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage), + SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => nameof(SysML2.NET.Core.POCO.Systems.Actions.IActionUsage), + SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => nameof(SysML2.NET.Core.POCO.Kernel.Behaviors.IStep), + SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => nameof(SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage), + SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage), + SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement), + SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => nameof(SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement), + SysML2.NET.Core.POCO.Root.Elements.IRelationship => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement), + _ => nameof(IElement), + }; + } + + /// + /// Determines whether is a structurally valid containment owner for + /// , based on the typed property declared by the bridge's + /// metaclass that subsets in the KerML/SysML2 + /// metamodel (e.g. an IFeatureMembership requires an IType owner). + /// + /// The bridge . + /// The candidate owner . + /// true if 's runtime type satisfies the owner-side + /// constraint; otherwise false. + public static bool QueryIsValidContainmentOwner(this IRelationship bridgeRelationship, IElement source) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => source is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage, + SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => source is SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage, + SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => source is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage, + SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => source is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage, + SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => source is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage, + SysML2.NET.Core.POCO.Kernel.Associations.IAssociation => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Kernel.Connectors.IConnector => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => source is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage, + SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => source is SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage, + SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => source is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => source is SysML2.NET.Core.POCO.Systems.Actions.IActionUsage, + SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => source is SysML2.NET.Core.POCO.Kernel.Behaviors.IStep, + SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => source is SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage, + SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortTyping => source is SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition, + SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => source is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => source is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Core.Features.IRedefinition => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => source is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage, + SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Root.Namespaces.IMembershipImport => source is SysML2.NET.Core.POCO.Root.Namespaces.IMembership, + SysML2.NET.Core.POCO.Root.Namespaces.INamespaceImport => source is SysML2.NET.Core.POCO.Root.Namespaces.INamespace, + SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => source is SysML2.NET.Core.POCO.Systems.Ports.IPortDefinition, + SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => source is SysML2.NET.Core.POCO.Core.Classifiers.IClassifier, + SysML2.NET.Core.POCO.Core.Features.ISubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Types.IConjugation => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Types.IDifferencing => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Types.IDisjoining => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Types.IIntersecting => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Types.ISpecialization => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Types.IUnioning => source is SysML2.NET.Core.POCO.Core.Types.IType, + _ => true, + }; + } + + /// + /// Returns the simple C# interface name of the expected owner type for the supplied + /// , used to compose the error message thrown by + /// AssignOwnership when the source is not a valid containment owner. + /// + /// The bridge . + /// The expected owner type's simple interface name. + public static string ExpectedContainmentOwnerTypeName(this IRelationship bridgeRelationship) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage), + SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage), + SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage), + SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage), + SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage), + SysML2.NET.Core.POCO.Kernel.Associations.IAssociation => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Kernel.Connectors.IConnector => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage), + SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => nameof(SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage), + SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => nameof(SysML2.NET.Core.POCO.Systems.Actions.IActionUsage), + SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => nameof(SysML2.NET.Core.POCO.Kernel.Behaviors.IStep), + SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => nameof(SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage), + SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortTyping => nameof(SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition), + SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Core.Features.IRedefinition => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage), + SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Root.Namespaces.IMembershipImport => nameof(SysML2.NET.Core.POCO.Root.Namespaces.IMembership), + SysML2.NET.Core.POCO.Root.Namespaces.INamespaceImport => nameof(SysML2.NET.Core.POCO.Root.Namespaces.INamespace), + SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => nameof(SysML2.NET.Core.POCO.Systems.Ports.IPortDefinition), + SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => nameof(SysML2.NET.Core.POCO.Core.Classifiers.IClassifier), + SysML2.NET.Core.POCO.Core.Features.ISubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Types.IConjugation => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Types.IDifferencing => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Types.IDisjoining => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Types.IIntersecting => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Types.ISpecialization => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Types.IUnioning => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + _ => nameof(IElement), + }; + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs new file mode 100644 index 00000000..6f94d6f8 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGeneratorTestFixture.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators +{ + using System.IO; + using System.Threading.Tasks; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators; + + [TestFixture] + public class UmlCorePocoValidationGeneratorTestFixture + { + private DirectoryInfo outputDirectory; + private UmlCorePocoValidationGenerator generator; + + [OneTimeSetUp] + public void OneTimeSetup() + { + var directoryInfo = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + var path = Path.Combine("UML", "_SysML2.NET.AutoGenExtensions.ElementExtensions"); + + this.outputDirectory = directoryInfo.CreateSubdirectory(path); + this.generator = new UmlCorePocoValidationGenerator(); + } + + [Test] + public async Task VerifyElementExtensionsAreGenerated() + { + await Assert.ThatAsync( + () => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.outputDirectory), + Throws.Nothing); + } + + [Test] + [Category("Expected")] + public async Task VerifyExpectedElementExtensionsMatches() + { + var generatedCode = await this.generator.GenerateElementExtensions( + GeneratorSetupFixture.XmiReaderResult, + this.outputDirectory); + + var expected = await File.ReadAllTextAsync(Path.Combine(TestContext.CurrentContext.TestDirectory, + "Expected/UML/Core/AutoGenExtensions/ElementExtensions.cs")); + + Assert.That(generatedCode, Is.EqualTo(expected)); + } + } +} diff --git a/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj b/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj index bc72a662..a0cee75b 100644 --- a/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj +++ b/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj @@ -19,7 +19,11 @@ + + + Always + Always diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs new file mode 100644 index 00000000..f4fedc4f --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs @@ -0,0 +1,544 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using SysML2.NET.CodeGenerator.Extensions; + + using uml4net.Classification; + using uml4net.HandleBars; + using uml4net.StructuredClassifiers; + using uml4net.xmi.Readers; + + using ClassHelper = SysML2.NET.CodeGenerator.HandleBarHelpers.ClassHelper; + using NamedElementHelper = SysML2.NET.CodeGenerator.HandleBarHelpers.NamedElementHelper; + using PropertyHelper = SysML2.NET.CodeGenerator.HandleBarHelpers.PropertyHelper; + + /// + /// The generates the auto-generated companion to the + /// hand-coded SysML2.NET.Extensions.ElementExtensions partial class. It emits a switch expression + /// (QueryIsValidForContainment) and a parallel name lookup (ExpectedContainmentTypeName) + /// that map each Relationship metaclass to the runtime-type check for the element it may carry as an + /// OwnedRelatedElement, derived from every typed composite property that subsets + /// IRelationship.OwnedRelatedElement. + /// + public class UmlCorePocoValidationGenerator : UmlHandleBarsGenerator + { + /// + /// The name of the Handlebars template that emits the partial-class file. + /// + private const string CorePocoValidationTemplateName = "core-poco-validation-template"; + + /// + /// The name of the file to write into the output directory. + /// + private const string OutputFileName = "ElementExtensions.cs"; + + /// + /// Generates the file in the supplied . + /// + /// The with the loaded UML model. + /// The target directory. + /// An awaitable . + /// If either argument is null. + public override async Task GenerateAsync(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory) + { + await this.GenerateElementExtensions(xmiReaderResult, outputDirectory); + } + + /// + /// Renders the file, writes it to , + /// and returns the generated source for assertion in expected-output tests (the "ExpectedResult" + /// principle used by sibling generators such as UmlPocoReferenceResolveExtensionGenerator). + /// + /// The with the loaded UML model. + /// The target directory. + /// The generated C# source, after CodeCleanup. + /// If either argument is null. + public async Task GenerateElementExtensions(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory) + { + ArgumentNullException.ThrowIfNull(xmiReaderResult); + ArgumentNullException.ThrowIfNull(outputDirectory); + + var payload = QueryContainmentRulePayload(xmiReaderResult); + + var template = this.Templates[CorePocoValidationTemplateName]; + var rendered = template(payload); + rendered = this.CodeCleanup(rendered); + + await WriteAsync(rendered, outputDirectory, OutputFileName); + + return rendered; + } + + /// + /// Register the custom Handlebars helpers used by the template. + /// + protected override void RegisterHelpers() + { + this.Handlebars.RegisterStringHelper(); + this.Handlebars.RegisterPropertyHelper(); + this.Handlebars.RegisterClassHelper(); + NamedElementHelper.RegisterNamedElementHelper(this.Handlebars); + PropertyHelper.RegisterPropertyHelper(this.Handlebars); + ClassHelper.RegisterClassHelper(this.Handlebars); + } + + /// + /// Register the code template. + /// + protected override void RegisterTemplates() + { + this.RegisterTemplate(CorePocoValidationTemplateName); + } + + /// + /// Walks the merged UML model and produces two parallel lists of : + /// one for the target-side narrowing (typed composite properties subsetting + /// OwnedRelatedElement) and one for the owner-side narrowing (typed properties subsetting + /// OwningRelatedElement with a type strictly narrower than Element). Each list is + /// returned ordered from most-specific to least-specific (deepest inheritance depth first), so + /// the emitted switch expressions match a class against its narrowest applicable interface first. + /// + /// The with the loaded UML model. + /// The payload with the two ordered rule lists. + private static ContainmentRulePayload QueryContainmentRulePayload(XmiReaderResult xmiReaderResult) + { + var (_, ownedRelatedElementXmiId, owningRelatedElementXmiId, elementXmiId) = + QueryRootContainmentMeta(xmiReaderResult); + + var seenClassXmiIds = new HashSet(); + var targetRows = new List<(IClass UmlClass, IProperty TypedProperty, int InheritanceDepth)>(); + var ownerRows = new List<(IClass UmlClass, IProperty TypedProperty, int InheritanceDepth)>(); + + foreach (var package in xmiReaderResult.QueryContainedAndImported("SysML")) + { + foreach (var umlClass in package.PackagedElement.OfType()) + { + if (!seenClassXmiIds.Add(umlClass.XmiId)) + { + continue; + } + + var inheritanceDepth = QueryInheritanceDepth(umlClass); + + var typedTarget = QueryTypedSubsettingProperty(umlClass, ownedRelatedElementXmiId, requireComposite: true); + + if (typedTarget != null) + { + targetRows.Add((umlClass, typedTarget, inheritanceDepth)); + } + + // Owner-side narrowing is meaningful only when the typed property's type is strictly + // narrower than Element — otherwise the arm would be equivalent to 'source is not null' + // which is exactly what the '_ => true' default already returns. + var typedOwner = QueryTypedSubsettingProperty(umlClass, owningRelatedElementXmiId, requireComposite: false); + + if (typedOwner != null && typedOwner.Type.XmiId != elementXmiId) + { + ownerRows.Add((umlClass, typedOwner, inheritanceDepth)); + } + } + } + + return new ContainmentRulePayload + { + TargetRules = targetRows + .OrderByDescending(x => x.InheritanceDepth) + .ThenBy(x => x.UmlClass.Name, StringComparer.Ordinal) + .Select(x => + { + var targetFqn = x.TypedProperty.Type.QueryFullyQualifiedTypeName(); + var targetSimpleName = "I" + x.TypedProperty.Type.Name; + + // When the expected target type is the root Element type, 'target is IElement' is + // equivalent to 'target is not null' (since the parameter is already typed IElement). + // Emit the latter to avoid the CA1508 / IDE0078 "type check succeeds on any + // not-null value" analyzer warning. + var targetCheck = x.TypedProperty.Type.XmiId == elementXmiId + ? "target is not null" + : $"target is {targetFqn}"; + + return new ContainmentRule + { + BridgeFqn = x.UmlClass.QueryFullyQualifiedTypeName(), + BridgeName = "I" + x.UmlClass.Name, + TargetFqn = targetFqn, + TargetName = targetSimpleName, + TargetCheck = targetCheck + }; + }) + .ToList(), + + OwnerRules = ownerRows + .OrderByDescending(x => x.InheritanceDepth) + .ThenBy(x => x.UmlClass.Name, StringComparer.Ordinal) + .Select(x => + { + var ownerFqn = x.TypedProperty.Type.QueryFullyQualifiedTypeName(); + var ownerSimpleName = "I" + x.TypedProperty.Type.Name; + + return new ContainmentRule + { + BridgeFqn = x.UmlClass.QueryFullyQualifiedTypeName(), + BridgeName = "I" + x.UmlClass.Name, + OwnerFqn = ownerFqn, + OwnerName = ownerSimpleName, + OwnerCheck = $"source is {ownerFqn}" + }; + }) + .ToList() + }; + } + + /// + /// Discovers the root containment metaclass and its two root containment properties purely from + /// the structure of the loaded UML model — no metaclass or property names are used. + /// + /// + /// The structural fingerprint of the root containment metaclass (KerML Relationship) is the + /// "self-referential containment" pattern: it declares two owned attributes whose declared Type is + /// a strict supertype of the metaclass itself. One attribute is composite (the contained-elements + /// side, ownedRelatedElement); the other is non-composite (the owner back-reference, + /// owningRelatedElement). The Type of those two attributes is the root Element + /// metaclass — i.e. one level above in the inheritance tree. + /// + /// The with the loaded UML model. + /// The relationship class together with the xmi ids of the two root containment + /// properties and the xmi id of the root element type they point to. + /// If no class in the loaded model matches the + /// self-referential containment fingerprint, or if more than one does. + private static (IClass RelationshipClass, string OwnedRelatedElementXmiId, string OwningRelatedElementXmiId, string ElementXmiId) + QueryRootContainmentMeta(XmiReaderResult xmiReaderResult) + { + var matches = new List<(IClass Class, IProperty Composite, IProperty BackRef)>(); + + foreach (var package in xmiReaderResult.QueryContainedAndImported("SysML")) + { + foreach (var candidateClass in package.PackagedElement.OfType()) + { + IProperty composite = null; + IProperty backRef = null; + + foreach (var attribute in candidateClass.OwnedAttribute) + { + // Derived attributes (e.g. the 'relatedElement' derived union on Relationship, + // which also has Type = Element) are not part of the structural fingerprint — + // we want the concrete association ends that subclasses subset, not the + // computed-union roots above them. + if (attribute.IsDerived) + { + continue; + } + + if (attribute.Type is not IClass attributeType) + { + continue; + } + + // The structural fingerprint: the owned attribute's Type is a strict supertype of + // the class that declares it. Only the root containment metaclass (Relationship) + // has this self-referential containment pattern. + if (!IsStrictDescendantOf(candidateClass, attributeType)) + { + continue; + } + + if (attribute.IsComposite && composite == null) + { + composite = attribute; + } + else if (!attribute.IsComposite && backRef == null) + { + backRef = attribute; + } + } + + if (composite != null && backRef != null) + { + matches.Add((candidateClass, composite, backRef)); + } + } + } + + if (matches.Count == 0) + { + throw new InvalidOperationException( + "Could not structurally identify the root containment metaclass in the loaded UML " + + "model: no class declares both a composite and a non-composite owned attribute whose " + + "Type is a strict supertype of itself."); + } + + if (matches.Count > 1) + { + throw new InvalidOperationException( + $"The structural fingerprint for the root containment metaclass is ambiguous in the " + + $"loaded UML model: {matches.Count} classes match. Expected exactly one."); + } + + var match = matches[0]; + return (match.Class, match.Composite.XmiId, match.BackRef.XmiId, ((IClass)match.Composite.Type).XmiId); + } + + /// + /// Returns true when is a strict (non-reflexive) descendant + /// of via transitive walks. + /// + /// The class whose ancestor chain is walked. + /// The class looked for as an ancestor. + /// true if is a strict ancestor of + /// ; otherwise false. + private static bool IsStrictDescendantOf(IClass candidateDescendant, IClass candidateAncestor) + { + var visited = new HashSet(); + var queue = new Queue(); + queue.Enqueue(candidateDescendant); + visited.Add(candidateDescendant.XmiId); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + foreach (var super in current.SuperClass) + { + if (super == null) + { + continue; + } + + if (super.XmiId == candidateAncestor.XmiId) + { + return true; + } + + if (visited.Add(super.XmiId)) + { + queue.Enqueue(super); + } + } + } + + return false; + } + + /// + /// Returns the most-specific owned attribute of whose + /// subsetting/redefinition chain transitively reaches the property identified by + /// . Returns null if no such attribute exists on the class + /// itself (inherited-only properties are intentionally ignored — the C# pattern-matching switch + /// already handles inheritance via interface-pattern fallthrough). + /// + /// The candidate class. + /// The xmi id of the root property in the subsetting chain. + /// When true, only composite-aggregation attributes are considered + /// (target-side OwnedRelatedElement chain). When false, any aggregation is accepted + /// (owner-side OwningRelatedElement chain, which is not composite). + /// The typed property, or null. + private static IProperty QueryTypedSubsettingProperty(IClass umlClass, string rootPropertyXmiId, bool requireComposite) + { + IProperty bestMatch = null; + var bestDepth = int.MinValue; + + foreach (var attribute in umlClass.OwnedAttribute) + { + if (requireComposite && !attribute.IsComposite) + { + continue; + } + + var depth = QueryDepthToRootProperty(attribute, rootPropertyXmiId); + + if (depth < 0) + { + continue; + } + + if (depth > bestDepth) + { + bestMatch = attribute; + bestDepth = depth; + } + } + + return bestMatch; + } + + /// + /// Computes the shortest distance from to the property identified by + /// via subsets and redefinitions. Returns -1 if the chain + /// does not reach the root. + /// + /// The starting property. + /// The xmi id of the root property in the subsetting chain. + /// The chain depth, or -1. + private static int QueryDepthToRootProperty(IProperty property, string rootPropertyXmiId) + { + var visited = new HashSet(); + var queue = new Queue<(IProperty Property, int Depth)>(); + queue.Enqueue((property, 0)); + visited.Add(property.XmiId); + + while (queue.Count > 0) + { + var (current, depth) = queue.Dequeue(); + + if (current.XmiId == rootPropertyXmiId) + { + return depth; + } + + foreach (var redefined in current.RedefinedProperty) + { + if (visited.Add(redefined.XmiId)) + { + queue.Enqueue((redefined, depth + 1)); + } + } + + foreach (var subsetted in current.SubsettedProperty) + { + if (visited.Add(subsetted.XmiId)) + { + queue.Enqueue((subsetted, depth + 1)); + } + } + } + + return -1; + } + + /// + /// Counts the number of distinct ancestor classes reachable from via + /// generalisation. Used as the sort key for the emitted switch expression so that more-specific + /// metaclasses appear first. + /// + /// The class to measure. + /// The transitive ancestor count. + private static int QueryInheritanceDepth(IClass umlClass) + { + var visited = new HashSet(); + var queue = new Queue(); + queue.Enqueue(umlClass); + visited.Add(umlClass.XmiId); + + var depth = 0; + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + foreach (var super in current.SuperClass.Where(super => super != null && visited.Add(super.XmiId))) + { + queue.Enqueue(super); + depth++; + } + } + + return depth; + } + } + + /// + /// Payload passed to the core-poco-validation-template Handlebars template. Holds two ordered + /// lists of : one for the target-side switch (QueryIsValidForContainment + /// and ExpectedContainmentTypeName) and one for the owner-side switch + /// (QueryIsValidContainmentOwner and ExpectedContainmentOwnerTypeName). + /// + public class ContainmentRulePayload + { + /// + /// Gets or sets the rules for the target-side switch (each bridge metaclass that declares a typed + /// composite subset of OwnedRelatedElement). + /// + public IReadOnlyList TargetRules { get; set; } + + /// + /// Gets or sets the rules for the owner-side switch (each bridge metaclass that declares a typed + /// subset of OwningRelatedElement with a type strictly narrower than Element). + /// + public IReadOnlyList OwnerRules { get; set; } + } + + /// + /// One arm of a generated switch expression: the bridge metaclass and either its expected target type + /// (when emitted into the target-side switch) or its expected owner type (when emitted into the + /// owner-side switch). A given instance carries only one side's fields. + /// + public class ContainmentRule + { + /// + /// Gets or sets the fully-qualified C# interface name of the bridge metaclass + /// (e.g. SysML2.NET.Core.POCO.Root.Namespaces.IFeatureMembership). + /// + public string BridgeFqn { get; set; } + + /// + /// Gets or sets the simple C# interface name of the bridge metaclass (e.g. IFeatureMembership). + /// + public string BridgeName { get; set; } + + /// + /// Gets or sets the fully-qualified C# interface name of the expected target type + /// (e.g. SysML2.NET.Core.POCO.Core.Features.IFeature). Set only on target-side rules. + /// + public string TargetFqn { get; set; } + + /// + /// Gets or sets the simple C# interface name of the expected target type (e.g. IFeature). + /// Set only on target-side rules. + /// + public string TargetName { get; set; } + + /// + /// Gets or sets the pre-composed C# expression rendered on the right-hand side of the + /// QueryIsValidForContainment switch arm. Equals target is {TargetFqn} for narrowed + /// target types, or target is not null when the expected target type is IElement + /// (the parameter type), to avoid the CA1508 / IDE0078 analyzer warning. + /// + public string TargetCheck { get; set; } + + /// + /// Gets or sets the fully-qualified C# interface name of the expected owner type + /// (e.g. SysML2.NET.Core.POCO.Core.Types.IType). Set only on owner-side rules. + /// + public string OwnerFqn { get; set; } + + /// + /// Gets or sets the simple C# interface name of the expected owner type (e.g. IType). + /// Set only on owner-side rules. + /// + public string OwnerName { get; set; } + + /// + /// Gets or sets the pre-composed C# expression rendered on the right-hand side of the + /// QueryIsValidContainmentOwner switch arm. Equals source is {OwnerFqn}. The + /// source is not null short-circuit is unnecessary on the owner side because rules whose + /// owner type is IElement are filtered out at generation time (they would be equivalent + /// to the _ => true default arm). + /// + public string OwnerCheck { get; set; } + } +} diff --git a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj index ce4c57da..a3fb8ef8 100644 --- a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj +++ b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj @@ -205,6 +205,9 @@ Always + + Always + Always diff --git a/SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs b/SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs new file mode 100644 index 00000000..18cee20a --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/Uml/core-poco-validation-template.hbs @@ -0,0 +1,115 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Extensions +{ + using SysML2.NET.Core.POCO.Root.Elements; + + /// + /// Auto-generated companion to the hand-coded partial class. + /// + public static partial class ElementExtensions + { + /// + /// Determines whether is a structurally valid element to be added as an + /// of , based on + /// the typed composite property declared by the bridge's metaclass in the KerML/SysML2 metamodel. + /// + /// The bridge . + /// The candidate . + /// true if 's runtime type satisfies the constraint; + /// otherwise false. + public static bool QueryIsValidForContainment(this IRelationship bridgeRelationship, IElement target) + { + return bridgeRelationship switch + { + {{#each TargetRules as | rule |}} + {{rule.BridgeFqn}} => {{rule.TargetCheck}}, + {{/each}} + _ => true, + }; + } + + /// + /// Returns the simple C# interface name of the expected target type for the supplied + /// , used to compose the error message thrown by + /// AssignOwnership when the target is not a valid containment target. + /// + /// The bridge . + /// The expected target type's simple interface name. + public static string ExpectedContainmentTypeName(this IRelationship bridgeRelationship) + { + return bridgeRelationship switch + { + {{#each TargetRules as | rule |}} + {{rule.BridgeFqn}} => nameof({{rule.TargetFqn}}), + {{/each}} + _ => nameof(IElement), + }; + } + + /// + /// Determines whether is a structurally valid containment owner for + /// , based on the typed property declared by the bridge's + /// metaclass that subsets in the KerML/SysML2 + /// metamodel (e.g. an IFeatureMembership requires an IType owner). + /// + /// The bridge . + /// The candidate owner . + /// true if 's runtime type satisfies the owner-side + /// constraint; otherwise false. + public static bool QueryIsValidContainmentOwner(this IRelationship bridgeRelationship, IElement source) + { + return bridgeRelationship switch + { + {{#each OwnerRules as | rule |}} + {{rule.BridgeFqn}} => {{rule.OwnerCheck}}, + {{/each}} + _ => true, + }; + } + + /// + /// Returns the simple C# interface name of the expected owner type for the supplied + /// , used to compose the error message thrown by + /// AssignOwnership when the source is not a valid containment owner. + /// + /// The bridge . + /// The expected owner type's simple interface name. + public static string ExpectedContainmentOwnerTypeName(this IRelationship bridgeRelationship) + { + return bridgeRelationship switch + { + {{#each OwnerRules as | rule |}} + {{rule.BridgeFqn}} => nameof({{rule.OwnerFqn}}), + {{/each}} + _ => nameof(IElement), + }; + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs index 2b928133..b6fed013 100644 --- a/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs @@ -44,17 +44,17 @@ public void VerifyComputeOwnedFeatureChaining() Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0)); var subsetting = new Subsetting(); - feature.AssignOwnership(subsetting, new Feature()); + feature.AssignOwnership(subsetting); Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0)); var chainingTarget1 = new Feature(); var chaining1 = new FeatureChaining { ChainingFeature = chainingTarget1 }; - feature.AssignOwnership(chaining1, new Feature()); + feature.AssignOwnership(chaining1); var chainingTarget2 = new Feature(); var chaining2 = new FeatureChaining { ChainingFeature = chainingTarget2 }; - feature.AssignOwnership(chaining2, new Feature()); + feature.AssignOwnership(chaining2); var result = feature.ComputeOwnedFeatureChaining(); @@ -77,12 +77,12 @@ public void VerifyComputeOwnedFeatureInverting() var otherFeature = new Feature(); var invertingPointingElsewhere = new FeatureInverting { FeatureInverted = otherFeature }; - feature.AssignOwnership(invertingPointingElsewhere, new Feature()); + feature.AssignOwnership(invertingPointingElsewhere); Assert.That(feature.ComputeOwnedFeatureInverting(), Has.Count.EqualTo(0)); var invertingPointingSelf = new FeatureInverting { FeatureInverted = feature }; - feature.AssignOwnership(invertingPointingSelf, new Feature()); + feature.AssignOwnership(invertingPointingSelf); var result = feature.ComputeOwnedFeatureInverting(); @@ -104,13 +104,13 @@ public void VerifyComputeOwnedTypeFeaturing() var otherFeature = new Feature(); var typeFeaturingPointingElsewhere = new TypeFeaturing { FeatureOfType = otherFeature }; - feature.AssignOwnership(typeFeaturingPointingElsewhere, new Feature()); + feature.AssignOwnership(typeFeaturingPointingElsewhere); Assert.That(feature.ComputeOwnedTypeFeaturing(), Has.Count.EqualTo(0)); var featuringType = new Type(); var typeFeaturingPointingSelf = new TypeFeaturing { FeatureOfType = feature, FeaturingType = featuringType }; - feature.AssignOwnership(typeFeaturingPointingSelf, new Feature()); + feature.AssignOwnership(typeFeaturingPointingSelf); var result = feature.ComputeOwnedTypeFeaturing(); @@ -131,18 +131,18 @@ public void VerifyComputeOwnedSubsetting() Assert.That(feature.ComputeOwnedSubsetting(), Has.Count.EqualTo(0)); var specialization = new Specialization(); - feature.AssignOwnership(specialization, new Feature()); + feature.AssignOwnership(specialization); Assert.That(feature.ComputeOwnedSubsetting(), Has.Count.EqualTo(0)); var subsetting = new Subsetting(); - feature.AssignOwnership(subsetting, new Feature()); + feature.AssignOwnership(subsetting); var redefinition = new Redefinition(); - feature.AssignOwnership(redefinition, new Feature()); + feature.AssignOwnership(redefinition); var referenceSubsetting = new ReferenceSubsetting(); - feature.AssignOwnership(referenceSubsetting, new Feature()); + feature.AssignOwnership(referenceSubsetting); var result = feature.ComputeOwnedSubsetting(); @@ -159,15 +159,15 @@ public void VerifyComputeOwnedRedefinition() Assert.That(feature.ComputeOwnedRedefinition(), Has.Count.EqualTo(0)); var subsetting = new Subsetting(); - feature.AssignOwnership(subsetting, new Feature()); + feature.AssignOwnership(subsetting); Assert.That(feature.ComputeOwnedRedefinition(), Has.Count.EqualTo(0)); var redefinition1 = new Redefinition(); - feature.AssignOwnership(redefinition1, new Feature()); + feature.AssignOwnership(redefinition1); var redefinition2 = new Redefinition(); - feature.AssignOwnership(redefinition2, new Feature()); + feature.AssignOwnership(redefinition2); var result = feature.ComputeOwnedRedefinition(); @@ -189,7 +189,7 @@ public void VerifyComputeOwnedReferenceSubsetting() Assert.That(feature.ComputeOwnedReferenceSubsetting(), Is.Null); var referenceSubsetting = new ReferenceSubsetting(); - feature.AssignOwnership(referenceSubsetting, new Feature()); + feature.AssignOwnership(referenceSubsetting); Assert.That(feature.ComputeOwnedReferenceSubsetting(), Is.SameAs(referenceSubsetting)); } @@ -204,7 +204,7 @@ public void VerifyComputeOwnedCrossSubsetting() Assert.That(feature.ComputeOwnedCrossSubsetting(), Is.Null); var crossSubsetting = new CrossSubsetting(); - feature.AssignOwnership(crossSubsetting, new Feature()); + feature.AssignOwnership(crossSubsetting); Assert.That(feature.ComputeOwnedCrossSubsetting(), Is.SameAs(crossSubsetting)); } @@ -219,17 +219,17 @@ public void VerifyComputeOwnedTyping() Assert.That(feature.ComputeOwnedTyping(), Has.Count.EqualTo(0)); var specialization = new Specialization(); - feature.AssignOwnership(specialization, new Feature()); + feature.AssignOwnership(specialization); Assert.That(feature.ComputeOwnedTyping(), Has.Count.EqualTo(0)); var type1 = new Type(); var typing1 = new FeatureTyping { Type = type1 }; - feature.AssignOwnership(typing1, new Feature()); + feature.AssignOwnership(typing1); var type2 = new Type(); var typing2 = new FeatureTyping { Type = type2 }; - feature.AssignOwnership(typing2, new Feature()); + feature.AssignOwnership(typing2); var result = feature.ComputeOwnedTyping(); @@ -252,11 +252,11 @@ public void VerifyComputeChainingFeature() var target1 = new Feature(); var chaining1 = new FeatureChaining { ChainingFeature = target1 }; - feature.AssignOwnership(chaining1, new Feature()); + feature.AssignOwnership(chaining1); var target2 = new Feature(); var chaining2 = new FeatureChaining { ChainingFeature = target2 }; - feature.AssignOwnership(chaining2, new Feature()); + feature.AssignOwnership(chaining2); var result = feature.ComputeChainingFeature(); @@ -325,11 +325,11 @@ public void VerifyComputeFeatureTarget() var target1 = new Feature(); var chaining1 = new FeatureChaining { ChainingFeature = target1 }; - feature.AssignOwnership(chaining1, new Feature()); + feature.AssignOwnership(chaining1); var lastTarget = new Feature(); var chaining2 = new FeatureChaining { ChainingFeature = lastTarget }; - feature.AssignOwnership(chaining2, new Feature()); + feature.AssignOwnership(chaining2); Assert.That(feature.ComputeFeatureTarget(), Is.SameAs(lastTarget)); } @@ -369,19 +369,19 @@ public void VerifyComputeCrossFeature() var crossedFeature = new Feature(); var crossSubsetting = new CrossSubsetting { CrossedFeature = crossedFeature }; - feature.AssignOwnership(crossSubsetting, new Feature()); + feature.AssignOwnership(crossSubsetting); Assert.That(feature.ComputeCrossFeature(), Is.Null); var chainingTarget1 = new Feature(); var chaining1 = new FeatureChaining { ChainingFeature = chainingTarget1 }; - crossedFeature.AssignOwnership(chaining1, new Feature()); + crossedFeature.AssignOwnership(chaining1); Assert.That(feature.ComputeCrossFeature(), Is.Null); var chainingTarget2 = new Feature(); var chaining2 = new FeatureChaining { ChainingFeature = chainingTarget2 }; - crossedFeature.AssignOwnership(chaining2, new Feature()); + crossedFeature.AssignOwnership(chaining2); Assert.That(feature.ComputeCrossFeature(), Is.SameAs(chainingTarget2)); } @@ -397,7 +397,7 @@ public void VerifyComputeFeaturingType() var theType = new Type(); var typeFeaturing = new TypeFeaturing { FeatureOfType = feature, FeaturingType = theType }; - feature.AssignOwnership(typeFeaturing, new Feature()); + feature.AssignOwnership(typeFeaturing); var result = feature.ComputeFeaturingType(); @@ -410,10 +410,10 @@ public void VerifyComputeFeaturingType() var chainingTarget = new Feature(); var chainingTargetType = new Type(); var chainingTypeFeaturing = new TypeFeaturing { FeatureOfType = chainingTarget, FeaturingType = chainingTargetType }; - chainingTarget.AssignOwnership(chainingTypeFeaturing, new Feature()); + chainingTarget.AssignOwnership(chainingTypeFeaturing); var chaining = new FeatureChaining { ChainingFeature = chainingTarget }; - feature.AssignOwnership(chaining, new Feature()); + feature.AssignOwnership(chaining); var resultWithChaining = feature.ComputeFeaturingType(); @@ -431,11 +431,11 @@ public void VerifyComputeType() var type1 = new Type(); var typing1 = new FeatureTyping { Type = type1 }; - feature.AssignOwnership(typing1, new Feature()); + feature.AssignOwnership(typing1); var type2 = new Type(); var typing2 = new FeatureTyping { Type = type2 }; - feature.AssignOwnership(typing2, new Feature()); + feature.AssignOwnership(typing2); var result = feature.ComputeType(); @@ -458,13 +458,13 @@ public void VerifyComputeNamingFeatureOperation() var redefinedFeature = new Feature { DeclaredName = "redefined" }; var redefinition = new Redefinition { RedefinedFeature = redefinedFeature }; - feature.AssignOwnership(redefinition, new Feature()); + feature.AssignOwnership(redefinition); Assert.That(feature.ComputeNamingFeatureOperation(), Is.SameAs(redefinedFeature)); var redefinedFeature2 = new Feature { DeclaredName = "redefined2" }; var redefinition2 = new Redefinition { RedefinedFeature = redefinedFeature2 }; - feature.AssignOwnership(redefinition2, new Feature()); + feature.AssignOwnership(redefinition2); Assert.That(feature.ComputeNamingFeatureOperation(), Is.SameAs(redefinedFeature)); } @@ -489,13 +489,13 @@ public void VerifyComputeRedefinedEffectiveShortNameOperation() var featureWithRedefinition = new Feature(); var namingFeature = new Feature { DeclaredShortName = "nfShort" }; var redefinition = new Redefinition { RedefinedFeature = namingFeature }; - featureWithRedefinition.AssignOwnership(redefinition, new Feature()); + featureWithRedefinition.AssignOwnership(redefinition); Assert.That(featureWithRedefinition.ComputeRedefinedEffectiveShortNameOperation(), Is.EqualTo("nfShort")); var featureNoNaming = new Feature(); var redefinitionNoTarget = new Redefinition { RedefinedFeature = new Feature() }; - featureNoNaming.AssignOwnership(redefinitionNoTarget, new Feature()); + featureNoNaming.AssignOwnership(redefinitionNoTarget); Assert.That(featureNoNaming.ComputeRedefinedEffectiveShortNameOperation(), Is.Null); } @@ -520,13 +520,13 @@ public void VerifyComputeRedefinedEffectiveNameOperation() var featureWithRedefinition = new Feature(); var namingFeature = new Feature { DeclaredName = "nfName" }; var redefinition = new Redefinition { RedefinedFeature = namingFeature }; - featureWithRedefinition.AssignOwnership(redefinition, new Feature()); + featureWithRedefinition.AssignOwnership(redefinition); Assert.That(featureWithRedefinition.ComputeRedefinedEffectiveNameOperation(), Is.EqualTo("nfName")); var featureNoNaming = new Feature(); var redefinitionNoTarget = new Redefinition { RedefinedFeature = new Feature() }; - featureNoNaming.AssignOwnership(redefinitionNoTarget, new Feature()); + featureNoNaming.AssignOwnership(redefinitionNoTarget); Assert.That(featureNoNaming.ComputeRedefinedEffectiveNameOperation(), Is.Null); } @@ -571,7 +571,7 @@ public void VerifyComputeRedefinesOperation() var otherFeature = new Feature(); var redefinition = new Redefinition { RedefinedFeature = otherFeature }; - feature.AssignOwnership(redefinition, new Feature()); + feature.AssignOwnership(redefinition); using (Assert.EnterMultipleScope()) { @@ -640,13 +640,13 @@ public void VerifyComputeIsCartesianProductOperation() var theType = new Type(); var typing = new FeatureTyping { Type = theType }; - feature.AssignOwnership(typing, new Feature()); + feature.AssignOwnership(typing); Assert.That(feature.ComputeIsCartesianProductOperation(), Is.False); var featuringType = new Type(); var typeFeaturing = new TypeFeaturing { FeatureOfType = feature, FeaturingType = featuringType }; - feature.AssignOwnership(typeFeaturing, new Feature()); + feature.AssignOwnership(typeFeaturing); Assert.That(feature.ComputeIsCartesianProductOperation(), Is.True); } @@ -664,7 +664,7 @@ public void VerifyComputeAsCartesianProductOperation() var theType = new Type(); var typing = new FeatureTyping { Type = theType }; - feature.AssignOwnership(typing, new Feature()); + feature.AssignOwnership(typing); result = feature.ComputeAsCartesianProductOperation(); @@ -673,7 +673,7 @@ public void VerifyComputeAsCartesianProductOperation() var featuringType = new Type(); var typeFeaturing = new TypeFeaturing { FeatureOfType = feature, FeaturingType = featuringType }; - feature.AssignOwnership(typeFeaturing, new Feature()); + feature.AssignOwnership(typeFeaturing); result = feature.ComputeAsCartesianProductOperation(); @@ -747,7 +747,7 @@ public void VerifyComputeAllRedefinedFeaturesOperation() var redefinedB = new Feature(); var redefinitionAB = new Redefinition { RedefinedFeature = redefinedB }; - feature.AssignOwnership(redefinitionAB, new Feature()); + feature.AssignOwnership(redefinitionAB); result = feature.ComputeAllRedefinedFeaturesOperation(); @@ -760,7 +760,7 @@ public void VerifyComputeAllRedefinedFeaturesOperation() var redefinedC = new Feature(); var redefinitionBC = new Redefinition { RedefinedFeature = redefinedC }; - redefinedB.AssignOwnership(redefinitionBC, new Feature()); + redefinedB.AssignOwnership(redefinitionBC); result = feature.ComputeAllRedefinedFeaturesOperation(); diff --git a/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs index f3472a73..e2a51dca 100644 --- a/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs @@ -48,7 +48,7 @@ public void VerifyComputeImportedMembership() importedNamespace.AssignOwnership(importedMembership, importedElement); var namespaceImport = new NamespaceImport { ImportedNamespace = importedNamespace }; - namespaceElement.AssignOwnership(namespaceImport, new Namespace()); + namespaceElement.AssignOwnership(namespaceImport); Assert.That(namespaceElement.ComputeImportedMembership(), Is.EquivalentTo([importedMembership])); } @@ -95,7 +95,7 @@ public void VerifyComputeOwnedImport() Assert.That(namespaceElement.ComputeOwnedImport(), Has.Count.EqualTo(0)); var namespaceImport = new NamespaceImport(); - namespaceElement.AssignOwnership(namespaceImport, new Namespace()); + namespaceElement.AssignOwnership(namespaceImport); Assert.That(namespaceElement.ComputeOwnedImport(), Is.EquivalentTo([namespaceImport])); } @@ -146,7 +146,7 @@ public void VerifyComputeNamesOfOperation() Assert.That(namespaceElement.ComputeNamesOfOperation(element), Has.Count.EqualTo(0)); var membership = new Membership { MemberName = "elementName", MemberShortName = "en", MemberElement = element }; - namespaceElement.AssignOwnership(membership, element); + namespaceElement.AssignOwnership(membership); Assert.That(namespaceElement.ComputeNamesOfOperation(element), Is.EquivalentTo(["en", "elementName"])); @@ -229,7 +229,7 @@ public void VerifyComputeImportedMembershipsOperation() importedNamespace.AssignOwnership(importedMembership, importedElement); var namespaceImport = new NamespaceImport { ImportedNamespace = importedNamespace }; - namespaceElement.AssignOwnership(namespaceImport, new Namespace()); + namespaceElement.AssignOwnership(namespaceImport); Assert.That(namespaceElement.ComputeImportedMembershipsOperation([]), Is.EquivalentTo([importedMembership])); diff --git a/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs index 9886cf4c..fc4743c4 100644 --- a/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/PackageExtensionsTestFixture.cs @@ -66,9 +66,8 @@ public void VerifyComputeRedefinedImportedMembershipsOperation() Assert.That(package.ComputeRedefinedImportedMembershipsOperation([]), Is.Empty); var importMember = new MembershipImport(); - var type = new Type(); - - package.AssignOwnership(importMember, type); + + package.AssignOwnership(importMember); Assert.That(()=> package.ComputeRedefinedImportedMembershipsOperation([]), Throws.InstanceOf()); var membership = new ElementFilterMembership(); diff --git a/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs b/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs index ee3940e1..50164e9d 100644 --- a/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs @@ -21,69 +21,275 @@ namespace SysML2.NET.Tests.Extensions { using System; - + + using NUnit.Framework; + using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Kernel.Expressions; + using SysML2.NET.Core.POCO.Kernel.FeatureValues; + using SysML2.NET.Core.POCO.Kernel.Packages; + using SysML2.NET.Core.POCO.Root.Annotations; + using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Core.POCO.Systems.Parts; using SysML2.NET.Extensions; - - using NUnit.Framework; + + using IContainedRelationship = SysML2.NET.Core.POCO.Root.Elements.IContainedRelationship; [TestFixture] public class ElementExtensionsTestFixture { private PartDefinition source; private FeatureMembership bridgeRelationship; + private Specialization referenceBridgeRelationship; private Feature target; [SetUp] public void SetUp() { - source = new PartDefinition(); - bridgeRelationship = new FeatureMembership(); - target = new Feature(); + this.source = new PartDefinition(); + this.bridgeRelationship = new FeatureMembership(); + // Specialization's owner-side narrowing is IType — PartDefinition IS-A IType, so the fixture + // source can validly own the reference bridge under the stricter owner-type guard. + this.referenceBridgeRelationship = new Specialization(); + this.target = new Feature(); } [Test] - public void AssignOwnership_WithNullSource_ThrowsArgumentNullException() + public void AssignOwnership_OwnerOnly_WithContainmentCycle_ThrowsInvalidOperationException() { - Assert.Throws(() => ElementExtensions.AssignOwnership(null, bridgeRelationship, target)); + // Arrange: bridge already directly contains source so that source.OwningRelationship == bridge. + ((IContainedRelationship)this.referenceBridgeRelationship).OwnedRelatedElement.Add(this.source); + + Assert.That( + () => this.source.AssignOwnership(this.referenceBridgeRelationship), + Throws.TypeOf().With.Message.Contains("Containment cycle")); + } + + [Test] + public void AssignOwnership_OwnerOnly_WithInvalidArguments_Throws() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(() => ElementExtensions.AssignOwnership(null, this.referenceBridgeRelationship), Throws.TypeOf()); + Assert.That(() => this.source.AssignOwnership(null), Throws.TypeOf()); + Assert.That(() => this.referenceBridgeRelationship.AssignOwnership(this.referenceBridgeRelationship), Throws.TypeOf()); + Assert.That(() => this.source.AssignOwnership(this.bridgeRelationship), Throws.TypeOf()); + } + } + + [Test] + public void AssignOwnership_OwnerOnly_WithMembershipAndNonNamespaceSource_ThrowsInvalidOperationException() + { + var nonNamespaceSource = new Comment(); + var membership = new Membership(); + + Assert.That( + () => nonNamespaceSource.AssignOwnership(membership), + Throws.TypeOf().With.Message.Contains("INamespace")); + } + + [Test] + public void AssignOwnership_OwnerOnly_WithValidParameters_AssignsOwnershipCorrectly() + { + this.source.AssignOwnership(this.referenceBridgeRelationship); + + using (Assert.EnterMultipleScope()) + { + Assert.That(this.referenceBridgeRelationship.OwningRelatedElement, Is.EqualTo(this.source)); + Assert.That(this.source.OwnedRelationship, Does.Contain(this.referenceBridgeRelationship)); + Assert.That(this.referenceBridgeRelationship.OwnedRelatedElement, Is.Empty); + } + } + + [Test] + public void AssignOwnership_WithAnnotation_AssignsOwnershipCorrectly() + { + var annotation = new Annotation(); + var comment = new Comment(); + + this.source.AssignOwnership(annotation, comment); + + using (Assert.EnterMultipleScope()) + { + Assert.That(annotation.OwningRelatedElement, Is.EqualTo(this.source)); + Assert.That(this.source.OwnedRelationship, Does.Contain(annotation)); + Assert.That(annotation.OwnedRelatedElement, Does.Contain(comment)); + } + } + + [Test] + public void AssignOwnership_WithBridgeEqualsTarget_ThrowsInvalidOperationException() + { + Assert.That( + () => this.source.AssignOwnership(this.bridgeRelationship, this.bridgeRelationship), + Throws.TypeOf()); + } + + [Test] + public void AssignOwnership_WithContainmentCycle_ThrowsInvalidOperationException() + { + // Scenario 1: bridge directly contains source. + var bridgeWithSourceInside = new FeatureMembership(); + var sourceInsideBridge = new PartDefinition(); + ((IContainedRelationship)bridgeWithSourceInside).OwnedRelatedElement.Add(sourceInsideBridge); + + // Scenario 2: target transitively contains source through a multi-level chain. + // Features are used throughout because FeatureMembership requires its target to be an IFeature + // (enforced by the QueryIsValidForContainment guard); Feature is also an INamespace, so it can + // serve as the owner of further FeatureMemberships in the chain. + var ancestorTarget = new Feature(); + var midMembership = new FeatureMembership(); + var midElement = new Feature(); + ancestorTarget.AssignOwnership(midMembership, midElement); + + var leafMembership = new FeatureMembership(); + var leafElement = new Feature(); + midElement.AssignOwnership(leafMembership, leafElement); + + var newBridge = new FeatureMembership(); + + using (Assert.EnterMultipleScope()) + { + Assert.That( + () => sourceInsideBridge.AssignOwnership(bridgeWithSourceInside, new Feature()), + Throws.TypeOf().With.Message.Contains("Containment cycle")); + + Assert.That( + () => leafElement.AssignOwnership(newBridge, ancestorTarget), + Throws.TypeOf().With.Message.Contains("Containment cycle")); + } + } + + [Test] + public void AssignOwnership_WithMembershipAndNonNamespaceSource_ThrowsInvalidOperationException() + { + // Comment is neither an INamespace nor an IType, so a generic OwningMembership owner check + // (requires INamespace) fires. + var nonNamespaceSource = new Comment(); + var owningMembership = new OwningMembership(); + var memberElement = new Feature(); + + Assert.That( + () => nonNamespaceSource.AssignOwnership(owningMembership, memberElement), + Throws.TypeOf().With.Message.Contains("INamespace")); + } + + [Test] + public void AssignOwnership_WithIncompatibleOwnerType_ThrowsInvalidOperationException() + { + // Package IS-A INamespace but NOT IType; FeatureMembership requires its owner to be IType + // per the KerML specification. The coarse "is IMembership → source is INamespace" check + // (the previous, lax behaviour) would have accepted this. The stricter generated guard rejects it. + var packageSource = new Package(); + var featureMembership = new FeatureMembership(); + var memberFeature = new Feature(); + + Assert.That( + () => packageSource.AssignOwnership(featureMembership, memberFeature), + Throws.TypeOf().With.Message.Contains("not a valid containment owner")); } [Test] public void AssignOwnership_WithNullBridge_ThrowsArgumentNullException() { - Assert.Throws(() => ElementExtensions.AssignOwnership(source, null, target)); + Assert.That( + () => this.source.AssignOwnership(null, this.target), + Throws.TypeOf()); + } + + [Test] + public void AssignOwnership_WithNullSource_ThrowsArgumentNullException() + { + Assert.That( + () => ElementExtensions.AssignOwnership(null, this.bridgeRelationship, this.target), + Throws.TypeOf()); } [Test] public void AssignOwnership_WithNullTarget_ThrowsArgumentNullException() { - Assert.Throws(() => ElementExtensions.AssignOwnership(source, bridgeRelationship, null)); + Assert.That( + () => this.source.AssignOwnership(this.bridgeRelationship, null), + Throws.TypeOf()); } [Test] - public void AssignOwnership_WithSourceEqualsTarget_ThrowsInvalidOperationException() + public void AssignOwnership_WithCompatibleTargetType_AssignsOwnershipCorrectly() { - Assert.Throws(() => ElementExtensions.AssignOwnership(source, bridgeRelationship, source)); + // FeatureValue's owner-side narrowing is IFeature, so the owner must be a Feature. + var featureValueOwner = new Feature(); + var featureValue = new FeatureValue(); + var literal = new LiteralBoolean(); + + var annotationOwner = new PartDefinition(); + var annotation = new Annotation(); + var comment = new Comment(); + + using (Assert.EnterMultipleScope()) + { + Assert.That( + () => featureValueOwner.AssignOwnership(featureValue, literal), + Throws.Nothing); + Assert.That( + () => annotationOwner.AssignOwnership(annotation, comment), + Throws.Nothing); + } } [Test] - public void AssignOwnership_WithBridgeEqualsTarget_ThrowsInvalidOperationException() + public void AssignOwnership_WithIncompatibleTargetType_ThrowsInvalidOperationException() + { + using (Assert.EnterMultipleScope()) + { + // FeatureMembership requires an IFeature target; Comment is not an IFeature. + Assert.That( + () => this.source.AssignOwnership(this.bridgeRelationship, new Comment()), + Throws.TypeOf().With.Message.Contains("not a valid containment target")); + + // FeatureValue requires an IExpression target; Feature is not an IExpression. + // The owner (a Feature) is valid since FeatureValue requires an IFeature owner. + var featureValueOwner = new Feature(); + var featureValue = new FeatureValue(); + Assert.That( + () => featureValueOwner.AssignOwnership(featureValue, new Feature()), + Throws.TypeOf().With.Message.Contains("not a valid containment target")); + + // Annotation requires an IAnnotatingElement target; Feature is not an IAnnotatingElement. + var annotationOwner = new PartDefinition(); + var annotation = new Annotation(); + Assert.That( + () => annotationOwner.AssignOwnership(annotation, new Feature()), + Throws.TypeOf().With.Message.Contains("not a valid containment target")); + } + } + + [Test] + public void AssignOwnership_WithReferenceOnlyRelationship_ThrowsInvalidOperationException() + { + Assert.That( + () => this.source.AssignOwnership(this.referenceBridgeRelationship, this.target), + Throws.TypeOf()); + } + + [Test] + public void AssignOwnership_WithSourceEqualsTarget_ThrowsInvalidOperationException() { - Assert.Throws(() => ElementExtensions.AssignOwnership(source, bridgeRelationship, bridgeRelationship)); + Assert.That( + () => this.source.AssignOwnership(this.bridgeRelationship, this.source), + Throws.TypeOf()); } [Test] public void AssignOwnership_WithValidParameters_AssignsOwnershipCorrectly() { - ElementExtensions.AssignOwnership(source, bridgeRelationship, target); + this.source.AssignOwnership(this.bridgeRelationship, this.target); using (Assert.EnterMultipleScope()) { - Assert.That(bridgeRelationship.OwningRelatedElement, Is.EqualTo(source)); - Assert.That(source.OwnedRelationship, Does.Contain(bridgeRelationship)); - Assert.That(bridgeRelationship.OwnedRelatedElement, Does.Contain(target)); + Assert.That(this.bridgeRelationship.OwningRelatedElement, Is.EqualTo(this.source)); + Assert.That(this.source.OwnedRelationship, Does.Contain(this.bridgeRelationship)); + Assert.That(this.bridgeRelationship.OwnedRelatedElement, Does.Contain(this.target)); } } } diff --git a/SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs b/SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs new file mode 100644 index 00000000..f4aa334a --- /dev/null +++ b/SysML2.NET/Extensions/AutoGenExtensions/ElementExtensions.cs @@ -0,0 +1,181 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Extensions +{ + using SysML2.NET.Core.POCO.Root.Elements; + + /// + /// Auto-generated companion to the hand-coded partial class. + /// + public static partial class ElementExtensions + { + /// + /// Determines whether is a structurally valid element to be added as an + /// of , based on + /// the typed composite property declared by the bridge's metaclass in the KerML/SysML2 metamodel. + /// + /// The bridge . + /// The candidate . + /// true if 's runtime type satisfies the constraint; + /// otherwise false. + public static bool QueryIsValidForContainment(this IRelationship bridgeRelationship, IElement target) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage, + SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage, + SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage, + SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => target is SysML2.NET.Core.POCO.Systems.Parts.IPartUsage, + SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage, + SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => target is SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage, + SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => target is SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage, + SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => target is SysML2.NET.Core.POCO.Systems.Actions.IActionUsage, + SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => target is SysML2.NET.Core.POCO.Kernel.Behaviors.IStep, + SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => target is SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage, + SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => target is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => target is SysML2.NET.Core.POCO.Kernel.Functions.IExpression, + SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => target is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage, + SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => target is not null, + SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => target is SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement, + SysML2.NET.Core.POCO.Root.Elements.IRelationship => target is not null, + _ => true, + }; + } + + /// + /// Returns the simple C# interface name of the expected target type for the supplied + /// , used to compose the error message thrown by + /// AssignOwnership when the target is not a valid containment target. + /// + /// The bridge . + /// The expected target type's simple interface name. + public static string ExpectedContainmentTypeName(this IRelationship bridgeRelationship) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Systems.Requirements.IActorMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage), + SysML2.NET.Core.POCO.Systems.Requirements.IFramedConcernMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IConcernUsage), + SysML2.NET.Core.POCO.Systems.VerificationCases.IRequirementVerificationMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage), + SysML2.NET.Core.POCO.Systems.Requirements.IStakeholderMembership => nameof(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage), + SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage), + SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Cases.IObjectiveMembership => nameof(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage), + SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Requirements.IRequirementConstraintMembership => nameof(SysML2.NET.Core.POCO.Systems.Constraints.IConstraintUsage), + SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Systems.States.IStateSubactionMembership => nameof(SysML2.NET.Core.POCO.Systems.Actions.IActionUsage), + SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership => nameof(SysML2.NET.Core.POCO.Kernel.Behaviors.IStep), + SysML2.NET.Core.POCO.Systems.Views.IViewRenderingMembership => nameof(SysML2.NET.Core.POCO.Systems.Views.IRenderingUsage), + SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Kernel.Functions.IExpression), + SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IVariantMembership => nameof(SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage), + SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement), + SysML2.NET.Core.POCO.Root.Annotations.IAnnotation => nameof(SysML2.NET.Core.POCO.Root.Annotations.IAnnotatingElement), + SysML2.NET.Core.POCO.Root.Elements.IRelationship => nameof(SysML2.NET.Core.POCO.Root.Elements.IElement), + _ => nameof(IElement), + }; + } + + /// + /// Determines whether is a structurally valid containment owner for + /// , based on the typed property declared by the bridge's + /// metaclass that subsets in the KerML/SysML2 + /// metamodel (e.g. an IFeatureMembership requires an IType owner). + /// + /// The bridge . + /// The candidate owner . + /// true if 's runtime type satisfies the owner-side + /// constraint; otherwise false. + public static bool QueryIsValidContainmentOwner(this IRelationship bridgeRelationship, IElement source) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => source is SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition, + SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => source is SysML2.NET.Core.POCO.Core.Classifiers.IClassifier, + SysML2.NET.Core.POCO.Core.Features.ISubsetting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Types.IConjugation => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Types.IDifferencing => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Types.IDisjoining => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Root.Namespaces.IImport => source is SysML2.NET.Core.POCO.Root.Namespaces.INamespace, + SysML2.NET.Core.POCO.Core.Types.IIntersecting => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Root.Namespaces.IMembership => source is SysML2.NET.Core.POCO.Root.Namespaces.INamespace, + SysML2.NET.Core.POCO.Core.Types.ISpecialization => source is SysML2.NET.Core.POCO.Core.Types.IType, + SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => source is SysML2.NET.Core.POCO.Core.Features.IFeature, + SysML2.NET.Core.POCO.Core.Types.IUnioning => source is SysML2.NET.Core.POCO.Core.Types.IType, + _ => true, + }; + } + + /// + /// Returns the simple C# interface name of the expected owner type for the supplied + /// , used to compose the error message thrown by + /// AssignOwnership when the source is not a valid containment owner. + /// + /// The bridge . + /// The expected owner type's simple interface name. + public static string ExpectedContainmentOwnerTypeName(this IRelationship bridgeRelationship) + { + return bridgeRelationship switch + { + SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Types.IFeatureMembership => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Features.IFeatureTyping => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Systems.Ports.IPortConjugation => nameof(SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition), + SysML2.NET.Core.POCO.Core.Classifiers.ISubclassification => nameof(SysML2.NET.Core.POCO.Core.Classifiers.IClassifier), + SysML2.NET.Core.POCO.Core.Features.ISubsetting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Types.IConjugation => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Types.IDifferencing => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Types.IDisjoining => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Features.IFeatureChaining => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Features.IFeatureInverting => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Root.Namespaces.IImport => nameof(SysML2.NET.Core.POCO.Root.Namespaces.INamespace), + SysML2.NET.Core.POCO.Core.Types.IIntersecting => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Root.Namespaces.IMembership => nameof(SysML2.NET.Core.POCO.Root.Namespaces.INamespace), + SysML2.NET.Core.POCO.Core.Types.ISpecialization => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing => nameof(SysML2.NET.Core.POCO.Core.Features.IFeature), + SysML2.NET.Core.POCO.Core.Types.IUnioning => nameof(SysML2.NET.Core.POCO.Core.Types.IType), + _ => nameof(IElement), + }; + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET/Extensions/ElementExtensions.cs b/SysML2.NET/Extensions/ElementExtensions.cs index 39ace00d..fb90cfaa 100644 --- a/SysML2.NET/Extensions/ElementExtensions.cs +++ b/SysML2.NET/Extensions/ElementExtensions.cs @@ -23,13 +23,76 @@ namespace SysML2.NET.Extensions using System; using System.Linq; + using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; /// /// Extension method for the interface /// - public static class ElementExtensions + public static partial class ElementExtensions { + /// + /// Assigns the containment ownership of the to a source , + /// without contributing any element to . + /// + /// The container that will own the + /// The to be owned by the + /// If or is null + /// If equals , + /// if is an (which always owns its + /// member element and therefore requires the three-argument overload), + /// if 's runtime type does not satisfy the typed property declared by the bridge's + /// metaclass for its (e.g. an IFeatureMembership requires + /// an IType source; an IMembership requires an INamespace source — per the KerML specification), + /// or if already transitively contains (which would + /// produce a containment cycle) + /// + /// Use this overload for the "owning reference" pattern, where the owns the relationship + /// but the relationship merely references its other related elements rather than containing them. Typical examples + /// are FeatureTyping, Subsetting, Redefinition, and Conjugation: the typed/specializing + /// feature owns the relationship, while the referenced type/specialized feature is not an owned related element. + /// For the "owning containment" pattern (e.g. FeatureMembership, OwningMembership), use the + /// overload instead. + /// + public static void AssignOwnership(this IElement source, IRelationship bridgeRelationship) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (bridgeRelationship == null) + { + throw new ArgumentNullException(nameof(bridgeRelationship)); + } + + if (source == bridgeRelationship) + { + throw new InvalidOperationException("The relationship can not own itself."); + } + + if (bridgeRelationship is IOwningMembership) + { + throw new InvalidOperationException( + $"The relationship of type '{bridgeRelationship.GetType().Name}' is an IOwningMembership and always owns a member element. Use the AssignOwnership(IElement, IRelationship, IElement) overload to supply the owned target element."); + } + + if (!bridgeRelationship.QueryIsValidContainmentOwner(source)) + { + throw new InvalidOperationException( + $"The source of type '{source.GetType().Name}' is not a valid containment owner for the relationship of type '{bridgeRelationship.GetType().Name}'; expected an instance of '{bridgeRelationship.ExpectedContainmentOwnerTypeName()}'."); + } + + if (IsContainedTransitivelyBy(source, bridgeRelationship)) + { + throw new InvalidOperationException( + $"Containment cycle: the supplied source of type '{source.GetType().Name}' is already transitively contained by the bridge relationship of type '{bridgeRelationship.GetType().Name}'."); + } + + AssignOwnershipCore(source, bridgeRelationship); + } + /// /// Assigns the containment ownership of a target to a source via the bridge /// @@ -37,9 +100,29 @@ public static class ElementExtensions /// The bridge /// The contained /// If one of the parameter is null - /// If the equals the - /// or if the equals the - /// Note: The source is the container element and the target is the containee + /// If the equals the , + /// if the equals the , + /// if is neither an nor an + /// (the only relationship metaclasses that subset + /// with composite aggregation), + /// if 's runtime type does not satisfy the typed property declared by the bridge's + /// metaclass for its (e.g. an IFeatureMembership + /// requires an IType source; an IMembership requires an INamespace source — per the + /// KerML specification), + /// if 's runtime type does not satisfy the typed composite property declared by the + /// bridge's metaclass for its (e.g. an + /// IFeatureMembership requires an IFeature target), + /// or if or already transitively contains + /// (which would produce a containment cycle) + /// + /// Use this overload for the "owning containment" pattern, where the contains + /// the as one of its . The only relationship + /// metaclasses that can carry an owned related element are (and all its specialisations + /// such as FeatureMembership, ParameterMembership, VariantMembership, FeatureValue, etc.) + /// and . For the "owning reference" pattern (e.g. FeatureTyping, Subsetting), + /// use the overload instead. + /// Note: the source is the container element and the target is the containee. + /// public static void AssignOwnership(this IElement source, IRelationship bridgeRelationship, IElement target) { if (source == null) @@ -57,6 +140,11 @@ public static void AssignOwnership(this IElement source, IRelationship bridgeRel throw new ArgumentNullException(nameof(target)); } + if (source == bridgeRelationship) + { + throw new InvalidOperationException("The relationship can not own itself."); + } + if (source == target) { throw new InvalidOperationException("The parent cannot own itself."); @@ -67,26 +155,134 @@ public static void AssignOwnership(this IElement source, IRelationship bridgeRel throw new InvalidOperationException("The relationship can not own itself."); } - // Missing logic: Target can not contain Source at any containment level + if (bridgeRelationship is not IOwningMembership and not IAnnotation) + { + throw new InvalidOperationException( + $"The relationship of type '{bridgeRelationship.GetType().Name}' is neither an IOwningMembership nor an IAnnotation and therefore cannot own a related element. Use the AssignOwnership(IElement, IRelationship) overload instead."); + } + + if (!bridgeRelationship.QueryIsValidContainmentOwner(source)) + { + throw new InvalidOperationException( + $"The source of type '{source.GetType().Name}' is not a valid containment owner for the relationship of type '{bridgeRelationship.GetType().Name}'; expected an instance of '{bridgeRelationship.ExpectedContainmentOwnerTypeName()}'."); + } + + if (!bridgeRelationship.QueryIsValidForContainment(target)) + { + throw new InvalidOperationException( + $"The target of type '{target.GetType().Name}' is not a valid containment target for the relationship of type '{bridgeRelationship.GetType().Name}'; expected an instance of '{ExpectedContainmentTypeName(bridgeRelationship)}'."); + } + + if (IsContainedTransitivelyBy(source, bridgeRelationship)) + { + throw new InvalidOperationException( + $"Containment cycle: the supplied source of type '{source.GetType().Name}' is already transitively contained by the bridge relationship of type '{bridgeRelationship.GetType().Name}'."); + } + + if (IsContainedTransitivelyBy(source, target)) + { + throw new InvalidOperationException( + $"Containment cycle: the supplied source of type '{source.GetType().Name}' is already transitively contained by the target of type '{target.GetType().Name}'."); + } + + AssignOwnershipCore(source, bridgeRelationship); + + if (target.OwningRelationship != null && target.OwningRelationship != bridgeRelationship) + { + ((IContainedRelationship)target.OwningRelationship).OwnedRelatedElement.Remove(target); + } + ((IContainedRelationship)bridgeRelationship).OwnedRelatedElement.Add(target); + } + + /// + /// Performs the owner-side mutation that attaches a to a + /// without applying any of the public-overload guard rules. + /// + /// The container + /// The to be attached to the + /// + /// Internal helper shared by both AssignOwnership public overloads. Callers are responsible for argument + /// validation; this method performs no checks of its own. + /// + private static void AssignOwnershipCore(IElement source, IRelationship bridgeRelationship) + { if (bridgeRelationship.OwningRelatedElement != null && bridgeRelationship.OwningRelatedElement != source) { ((IContainedElement)bridgeRelationship.OwningRelatedElement).OwnedRelationship.Remove(bridgeRelationship); } ((IContainedRelationship)bridgeRelationship).OwningRelatedElement = source; - + if (!source.OwnedRelationship.Contains(bridgeRelationship)) { ((IContainedElement)source).OwnedRelationship.Add(bridgeRelationship); } + } - if (target.OwningRelationship != null && target.OwningRelationship != bridgeRelationship) + /// + /// Determines whether is currently — directly or transitively — contained by + /// via the chain of and + /// pointers. + /// + /// The whose ancestor chain is walked + /// The looked for in the ancestor chain + /// true if appears as an + /// at any level above ; otherwise false + /// + /// Used by the public AssignOwnership overloads to reject containment cycles before any state mutation. + /// + private static bool IsContainedTransitivelyBy(IElement element, IRelationship candidateBridge) + { + var current = element; + + while (current != null) { - ((IContainedRelationship)target.OwningRelationship).OwnedRelatedElement.Remove(target); - } + var owningRelationship = current.OwningRelationship; - ((IContainedRelationship)bridgeRelationship).OwnedRelatedElement.Add(target); + if (owningRelationship == null) + { + return false; + } + + if (owningRelationship == candidateBridge) + { + return true; + } + + current = owningRelationship.OwningRelatedElement; + } + + return false; + } + + /// + /// Determines whether is currently — directly or transitively — contained by + /// via the chain of and + /// pointers. + /// + /// The whose ancestor chain is walked + /// The looked for in the ancestor chain + /// true if appears as an ancestor of ; + /// otherwise false + /// + /// Used by the public AssignOwnership overloads to reject containment cycles before any state mutation. + /// + private static bool IsContainedTransitivelyBy(IElement element, IElement candidateAncestor) + { + var current = element.OwningRelationship?.OwningRelatedElement; + + while (current != null) + { + if (current == candidateAncestor) + { + return true; + } + + current = current.OwningRelationship?.OwningRelatedElement; + } + + return false; } } } diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..7c6bf617 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,248 @@ +# TESTING.md — Unit-test conventions for SysML2.NET + +> **When to read this file:** load it whenever you are writing or modifying unit tests in any `*.Tests/` project of this solution. It is the authoritative companion to `CLAUDE.md` for everything test-related. Outside of test authoring, you do not need it. + +This document captures the conventions enforced for the NUnit test fixtures across `SysML2.NET.Tests/`, `SysML2.NET.Serializer.Json.Tests/`, `SysML2.NET.Serializer.Xmi.Tests/`, `SysML2.NET.Extensions.Tests/`, `SysML2.NET.CodeGenerator.Tests/`, and every other `*.Tests` project. The rules below have been distilled from explicit author-review feedback — diverging from them produces churn and PR back-and-forth, so treat them as binding. + +--- + +## 1. Test framework + +- **Framework:** NUnit +- **Fixture attribute:** `[TestFixture]` +- **Test attribute:** `[Test]` +- **Project layout:** mirror the production namespace under the test project (e.g. `SysML2.NET/Extend/FeatureExtensions.cs` → `SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs`). + +--- + +## 2. One `[Test]` per method-under-test (default) + +**Default to a single `[Test]` method per `Compute*` / method-under-test** and pack every scenario — happy path, edge cases, null guards, alternate inputs — into multiple `Assert.That` calls inside that one test. + +**Why this is the default:** keeps the test list compact, removes duplicated arrange boilerplate, and makes the intent obvious — one `Compute*` operation has one combined coverage test. It works particularly well for the `Compute*` derivation tests where scenarios build on one shared subject incrementally (null guard → empty subject → wrong-target negative → positive populated case → multi-element ordered case). + +### When separated `[Test]` methods ARE appropriate + +The combined-form is the default, **not absolute**. Split into separate `[Test]` methods when **each scenario has a genuinely distinct, complex setup** that would tangle if packed into one test: + +- different bridge / relationship metaclass per scenario, +- a multi-step containment chain that exists only for one scenario, +- an incompatible-source-or-target shape that requires its own local POCO fixtures. + +In those cases, separate `[Test]` methods with descriptive scenario suffixes are clearer than one mega-test with branching arrange blocks. The canonical *separated* example is `SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs` — each `AssignOwnership_*` method exercises a distinct scenario (cycle, incompatible owner type, incompatible target type, valid parameters, …) with its own local construction. + +Rule of thumb: if you find yourself naming locals `feature1` / `feature2` / `bridge1` / `bridge2` or writing more than ~3 lines of arrange between asserts, that scenario probably wants its own `[Test]`. + +### Canonical example + +`SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs`: + +```csharp +[Test] +public void VerifyComputeOwnedFeatureChaining() +{ + Assert.That(() => ((IFeature)null).ComputeOwnedFeatureChaining(), Throws.TypeOf()); + + var feature = new Feature(); + + Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0)); + + var subsetting = new Subsetting(); + feature.AssignOwnership(subsetting); + + Assert.That(feature.ComputeOwnedFeatureChaining(), Has.Count.EqualTo(0)); + + var chainingTarget1 = new Feature(); + var chaining1 = new FeatureChaining { ChainingFeature = chainingTarget1 }; + feature.AssignOwnership(chaining1); + + var chainingTarget2 = new Feature(); + var chaining2 = new FeatureChaining { ChainingFeature = chainingTarget2 }; + feature.AssignOwnership(chaining2); + + var result = feature.ComputeOwnedFeatureChaining(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0], Is.SameAs(chaining1)); + Assert.That(result[1], Is.SameAs(chaining2)); + } +} +``` + +One test, five logical scenarios: null subject → empty subject → unrelated-ownership subject → first populated case → multiple-element ordered case. + +--- + +## 3. Cover positive AND negative cases in the same test + +Each `[Test]` MUST exercise both directions of the method-under-test. Build up state incrementally and assert the delta after each arrange step. + +**Negative cases (always include where applicable):** + +| Negative scenario | Assertion | +| -------------------------- | ------------------------------------------------------------------------ | +| Null subject | `Assert.That(() => …, Throws.TypeOf())` | +| Empty / unpopulated subject | `Assert.That(result, Has.Count.EqualTo(0))` | +| Wrong-target scenario | populated subject whose owned elements don't match the rule's filter — assert `Has.Count.EqualTo(0)` | +| Out-of-scope stub | `Assert.That(() => …, Throws.TypeOf())` — see §9 | + +**Positive cases:** populated subject, multiple elements with verified ordering, dedup semantics where the OCL says so. + +**Pre/post-arrange assert ladder** — see `SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs`: + +```csharp +[Test] +public void VerifyComputeImportedMembership() +{ + Assert.That(() => ((INamespace)null).ComputeImportedMembership(), Throws.TypeOf()); + + var namespaceElement = new Namespace(); + + Assert.That(namespaceElement.ComputeImportedMembership(), Has.Count.EqualTo(0)); + + var importedNamespace = new Namespace(); + var importedElement = new Definition { DeclaredName = "imported" }; + var importedMembership = new OwningMembership { Visibility = VisibilityKind.Public }; + importedNamespace.AssignOwnership(importedMembership, importedElement); + + var namespaceImport = new NamespaceImport { ImportedNamespace = importedNamespace }; + namespaceElement.AssignOwnership(namespaceImport); + + Assert.That(namespaceElement.ComputeImportedMembership(), Is.EquivalentTo([importedMembership])); +} +``` + +The wrong-target negative case is illustrated in `FeatureExtensionsTestFixture.VerifyComputeOwnedFeatureInverting` — a `FeatureInverting` pointing at *another* feature is added first (asserted to produce count 0) **before** the positive-case `FeatureInverting` pointing at the subject is added. + +--- + +## 4. Always use `Assert.That` — no legacy forms + +Every assertion (including exception assertions) MUST use the fluent `Assert.That(...)` form. + +**Forbidden legacy forms:** + +| Don't write | Write instead | +| ------------------------ | ------------------------------------------------------------------------ | +| `Assert.Throws(…)` | `Assert.That(() => …, Throws.TypeOf())` | +| `Assert.IsTrue(x)` | `Assert.That(x, Is.True)` | +| `Assert.IsFalse(x)` | `Assert.That(x, Is.False)` | +| `Assert.AreEqual(a, b)` | `Assert.That(b, Is.EqualTo(a))` | +| `Assert.IsNull(x)` | `Assert.That(x, Is.Null)` | +| `Assert.IsNotNull(x)` | `Assert.That(x, Is.Not.Null)` | + +**Exception-message assertions** are still a single fluent chain — no scope wrapper: + +```csharp +Assert.That(() => parser.Parse(input), + Throws.TypeOf().With.Message.Contains("unexpected token")); +``` + +**Why:** consistency and readability. The mixed-style `Assert.Throws` / `Assert.IsTrue` family was explicitly called out as non-idiomatic. + +--- + +## 5. `Assert.EnterMultipleScope` — use only for consecutive asserts + +Use `using (Assert.EnterMultipleScope()) { … }` **only** when **two or more consecutive** `Assert.That` calls follow each other and you want all of them to be evaluated even if earlier ones fail. + +**Do:** + +```csharp +using (Assert.EnterMultipleScope()) +{ + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0], Is.SameAs(chaining1)); + Assert.That(result[1], Is.SameAs(chaining2)); +} +``` + +**Don't** wrap a single fluent assertion in a scope — even when it is a long chain like `Throws.TypeOf().With.Message.Contains("…")`. A single assertion does not need it. + +**Don't** put the early null-guard `Assert.That` inside a scope that ends before the next `Assert.That` — those are standalone and stay outside any scope. + +--- + +## 6. Test naming + +**Combined-form tests (the default — §2):** + +- One test method = `public void Verify{MethodUnderTest}()` (e.g. `VerifyComputeOwnedFeatureChaining`, `VerifyComputeImportedMembership`). +- Matches the existing convention across `SysML2.NET.Tests/Extend/*` and is what reviewers will look for. +- Do **not** suffix with scenario names — scenarios live inside the body. + +**Separated-form tests (the §2 exception):** + +- Use `{MethodUnderTest}_{ScenarioDescription}_{ExpectedOutcome}` (e.g. `AssignOwnership_WithIncompatibleOwnerType_ThrowsInvalidOperationException`, `AssignOwnership_WithContainmentCycle_ThrowsInvalidOperationException`). +- Each method should be self-explanatory from its name alone — the scenario is in the title because it's distinct enough to deserve its own test. + +--- + +## 7. Arrange / Act / Assert inside a combined test + +- Separate arrange / act / assert blocks with **blank lines**. +- Add short inline comments only when the delta from the previous step is non-obvious. +- Re-call the method-under-test after each new arrange step so each `Assert.That` describes one increment of state. +- Reuse the same locals (`feature`, `namespaceElement`, `result`) across the test — don't fork into `feature1` / `feature2` unless you really need two independent subjects. + +--- + +## 8. Assertion idiom preferences + +| Concern | Prefer | Avoid | +| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------- | +| Collection count | `Has.Count.EqualTo(n)` | `result.Count, Is.EqualTo(n)` | +| Reference equality (POCO) | `Is.SameAs(expected)` | `Is.EqualTo(expected)` (relies on `Equals`) | +| Order-irrelevant collection equality | `Is.EquivalentTo([…])` | `Is.EqualTo([…])` (forces order) | +| Order-relevant collection equality | `Is.EqualTo([…])` | hand-rolled `for (int i …)` loops | +| First / last element | `result[0]` / `result[^1]` (indexer) | `result.First()` / `result.Last()` (LINQ) | +| Range / slice | `result[1..^1]` | `result.Skip(1).Take(n)` | +| String empty/null | `Is.Null.Or.Empty` | `string.IsNullOrEmpty(x), Is.True` | +| Substring | `Does.Contain("…")` or `Contains.Substring("…")` | `x.Contains("…"), Is.True` | + +The LINQ/indexer preference aligns with the project-wide quality rule documented in `CLAUDE.md` ("Prefer indexer syntax and range syntax over LINQ methods when applicable"). + +--- + +## 9. Scope discipline — assert, don't fix + +If a test you are writing crosses into a method that currently throws `NotSupportedException` because it's an out-of-scope stub, **assert that exception** — do **not** implement the stub to make the test pass: + +```csharp +Assert.That(() => member.ComputeOwnedMemberFeature(), + Throws.TypeOf()); +``` + +When the stub is implemented later (in its own scoped change), the assertion will be replaced with the real positive case in the same `[Test]`. + +This is the testing-side companion to the broader scope-discipline feedback: a task scoped to file X must not silently grow to also modify file Y, even if file Y is "one line away from working". + +--- + +## 10. Anti-pattern checklist (what NOT to do) + +- ❌ Splitting one method-under-test into many `…_WhenX_DoesY` tests **when the scenarios share setup and could trivially combine** (combined-form is the default — see §2). Splitting is acceptable when each scenario has a genuinely distinct, complex setup. +- ❌ `Assert.Throws` / `Assert.IsTrue` / `Assert.AreEqual` / `Assert.IsNull` (any legacy NUnit API). +- ❌ Wrapping a single `Assert.That` inside `Assert.EnterMultipleScope`. +- ❌ Putting standalone, non-adjacent asserts inside a scope that's already closed. +- ❌ Suffixing the combined-form `Verify{MethodUnderTest}` test name with a scenario (`VerifyComputeFooWhenNull`) — scenarios live in the body. (The separated-form `{Method}_{Scenario}_{Outcome}` style is the §6 exception and is fine when §2's separated-form criteria apply.) +- ❌ Implementing an out-of-scope stub from within a test fixture change. +- ❌ Asserting only the positive case (or only the negative case) — every test covers both. +- ❌ `result.First()` / `result.Last()` / `result.Count() == 0` where indexer / `Has.Count.EqualTo` works. + +--- + +## 11. Reference fixtures + +When in doubt, model new fixtures on these (they reflect the current canonical styles): + +**Combined-form (the default — §2):** + +- `SysML2.NET.Tests/Extend/FeatureExtensionsTestFixture.cs` — null guard → empty subject → wrong-target negative → populated positive → multi-assert scope. +- `SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs` — pre/post-arrange assert ladder, `Is.EquivalentTo([…])` collection idiom, no `EnterMultipleScope` when assertions are non-consecutive. + +**Separated-form (the §2 exception):** + +- `SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs` — each `AssignOwnership_*` `[Test]` exercises a distinct scenario (cycle, incompatible owner type, incompatible target type, null guards, valid parameters, reference-only bridge, …) with its own local POCO construction. Scenario-suffixed names are appropriate here because each scenario is too distinct to combine cleanly. From ef39a125ebf7951510cc188d2e59eced323dc50f Mon Sep 17 00:00:00 2001 From: atheate Date: Mon, 11 May 2026 15:53:55 +0200 Subject: [PATCH 2/2] Fix SQ issues and rebase --- .../UmlCorePocoValidationGenerator.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs index f4fedc4f..7fc468d4 100644 --- a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCorePocoValidationGenerator.cs @@ -410,20 +410,14 @@ private static int QueryDepthToRootProperty(IProperty property, string rootPrope return depth; } - foreach (var redefined in current.RedefinedProperty) + foreach (var redefined in current.RedefinedProperty.Where(redefined => visited.Add(redefined.XmiId))) { - if (visited.Add(redefined.XmiId)) - { - queue.Enqueue((redefined, depth + 1)); - } + queue.Enqueue((redefined, depth + 1)); } - foreach (var subsetted in current.SubsettedProperty) + foreach (var subsetted in current.SubsettedProperty.Where(subsetted => visited.Add(subsetted.XmiId))) { - if (visited.Add(subsetted.XmiId)) - { - queue.Enqueue((subsetted, depth + 1)); - } + queue.Enqueue((subsetted, depth + 1)); } }