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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,28 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package
- 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
- When invoking an operation or derived property on a POCO from inside an extension method, call the POCO's instance member (e.g. `subject.IsDistinguishableFrom(other)`, `subject.qualifiedName`), NOT the static `ComputeXxxOperation` / `ComputeXxx` extension method. Virtual dispatch on the POCO honors operation/property REDEFINITION in subclass POCOs; calling the static extension directly bypasses dispatch and silently skips overrides. The static-extension form is reserved EXCLUSIVELY for the C# translation of OCL `self.oclAsType(SuperType).method()` — an explicit upcast that mandates targeting the SuperType's body (e.g. `Usage::namingFeature()` → `FeatureExtensions.ComputeNamingFeatureOperation(usage)`; `OwningMembership::path()` → `RelationshipExtensions.ComputeRedefinedPathOperation(owningMembership)`)
- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage collections are `[0..*]` — NEVER cardinality-limited.** The [1..1] / [0..1] multiplicities that appear in the metamodel apply to *derived* / *redefined* properties (e.g. `OwningMembership::ownedMemberElement`, `FeatureMembership::ownedMemberFeature`, `SubjectMembership::ownedSubjectParameter`), NOT to the underlying storage. When implementing such a derivation, **project from the collection — do not assume positional indexing**. The canonical "in-between" pattern is *filter-by-type-then-validate-count*: project with `OfType<ITargetType>()`, then validate the projection count against the **derived property's declared multiplicity** (read it from the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface, or directly from the UML XMI). The failure mode depends on the multiplicity:

| Multiplicity | Empty projection | Single-match projection | 2+ match projection |
|---|---|---|---|
| `[1..1]` (lowerValue=1, upperValue=1) | `throw IncompleteModelException` | return the match | `throw IncompleteModelException` |
| `[0..1]` (lowerValue=0, upperValue=1) | `return null` | return the match | `throw IncompleteModelException` |
| `[0..*]` / `[1..*]` | (use `List<T>` projection; not this pattern) | n/a | n/a |

`IncompleteModelException` is the loud signal to SDK users that the model is malformed — DO NOT swallow it as `null` when the multiplicity is `[1..1]`, and DO NOT raise it for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property).

```csharp
// [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage)
var matches = subject.OwnedRelatedElement.OfType<ITargetType>().ToList();

return matches.Count == 1
? matches[0]
: throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element of type {nameof(ITargetType)}");

// [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement)
return subject.OwnedRelatedElement.Count == 1
? subject.OwnedRelatedElement[0]
: throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element");
```

Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern silently drops the correctly-typed element when it does not sit at index 0 (e.g. when an `IAnnotation` target is also present, since `AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`). Same rule applies to any other derived property that subsets one of these two `[0..*]` storage collections.
32 changes: 27 additions & 5 deletions SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,49 @@ public void VerifyComputeOwnedMemberFeature()
{
Assert.That(() => ((IFeatureMembership)null).ComputeOwnedMemberFeature(), Throws.TypeOf<ArgumentNullException>());

// Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var featureMembership = new FeatureMembership();

Assert.That(() => featureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf<IncompleteModelException>());

// Single IFeature wired via the public API → returned.
var owningType = new Type();
var feature = new Feature();

owningType.AssignOwnership(featureMembership, feature);

Assert.That(featureMembership.ComputeOwnedMemberFeature(), Is.SameAs(feature));

// NOTE: wiring a non-IFeature element as the sole OwnedRelatedElement is not possible via the
// public AssignOwnership API (it validates that FeatureMembership requires an IFeature target).
// To cover the as-cast-returns-null path we directly set OwningRelatedElement on a fresh
// membership so that OwnedRelatedElement[0] is a plain Namespace (which is not an IFeature).
// Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var twoFeatureMembership = new FeatureMembership();
var firstFeature = new Feature();
var secondFeature = new Feature();

((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(firstFeature);
((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature);

Assert.That(() => twoFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf<IncompleteModelException>());

// Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace).
// The OfType<IFeature>() projection MUST pick out the IFeature regardless of its position
// (this is the core robustness guarantee — never positionally index the unfiltered collection).
var mixedMembership = new FeatureMembership();
var siblingNonFeature = new Namespace();
var mixedFeature = new Feature();

((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonFeature);
((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedFeature);

Assert.That(mixedMembership.ComputeOwnedMemberFeature(), Is.SameAs(mixedFeature));

// OwnedRelatedElement populated with non-IFeature element(s) only → no IFeature match:
// [1..1] violation, throws IncompleteModelException.
var nonFeatureMembership = new FeatureMembership();
var nonFeatureElement = new Namespace();

((IContainedRelationship)nonFeatureMembership).OwnedRelatedElement.Add(nonFeatureElement);

Assert.That(nonFeatureMembership.ComputeOwnedMemberFeature(), Is.Null);
Assert.That(() => nonFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf<IncompleteModelException>());
}

[Test]
Expand Down
34 changes: 32 additions & 2 deletions SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace SysML2.NET.Tests.Extend
using SysML2.NET.Core.POCO.Kernel.FeatureValues;
using SysML2.NET.Core.POCO.Root.Elements;
using SysML2.NET.Core.POCO.Root.Namespaces;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

[TestFixture]
Expand Down Expand Up @@ -61,6 +62,12 @@ public void VerifyComputeValue()
{
Assert.That(() => ((IFeatureValue)null).ComputeValue(), Throws.TypeOf<ArgumentNullException>());

// Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var emptyFeatureValue = new FeatureValue();

Assert.That(() => emptyFeatureValue.ComputeValue(), Throws.TypeOf<IncompleteModelException>());

// Single IExpression wired via the public API → returned.
var feature = new Feature();
var featureValue = new FeatureValue();
var literalBoolean = new LiteralBoolean();
Expand All @@ -69,13 +76,36 @@ public void VerifyComputeValue()

Assert.That(featureValue.ComputeValue(), Is.SameAs(literalBoolean));

// Non-Expression owned member: direct field bypass — the cast must return null.
// Two IExpressions in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var twoExprFeatureValue = new FeatureValue();
var firstExpression = new LiteralBoolean();
var secondExpression = new LiteralBoolean();

((IContainedRelationship)twoExprFeatureValue).OwnedRelatedElement.Add(firstExpression);
((IContainedRelationship)twoExprFeatureValue).OwnedRelatedElement.Add(secondExpression);

Assert.That(() => twoExprFeatureValue.ComputeValue(), Throws.TypeOf<IncompleteModelException>());

// Mixed-type owned related elements: exactly one IExpression alongside a non-IExpression (Namespace).
// The OfType<IExpression>() projection MUST pick out the IExpression regardless of its position
// (this is the core robustness guarantee — never positionally index the unfiltered collection).
var mixedFeatureValue = new FeatureValue();
var siblingNonExpression = new Namespace();
var mixedExpression = new LiteralBoolean();

((IContainedRelationship)mixedFeatureValue).OwnedRelatedElement.Add(siblingNonExpression);
((IContainedRelationship)mixedFeatureValue).OwnedRelatedElement.Add(mixedExpression);

Assert.That(mixedFeatureValue.ComputeValue(), Is.SameAs(mixedExpression));

// OwnedRelatedElement populated with non-IExpression element(s) only → no IExpression match:
// [1..1] violation, throws IncompleteModelException.
var nonExprFeatureValue = new FeatureValue();
var nonExprElement = new Namespace();

((IContainedRelationship)nonExprFeatureValue).OwnedRelatedElement.Add(nonExprElement);

Assert.That(nonExprFeatureValue.ComputeValue(), Is.Null);
Assert.That(() => nonExprFeatureValue.ComputeValue(), Throws.TypeOf<IncompleteModelException>());
}
}
}
33 changes: 22 additions & 11 deletions SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,37 +43,48 @@ public void VerifyComputeOwnedMemberParameter()
{
Assert.That(() => ((IParameterMembership)null).ComputeOwnedMemberParameter(), Throws.TypeOf<ArgumentNullException>());

// Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var parameterMembership = new ParameterMembership();

Assert.That(() => parameterMembership.ComputeOwnedMemberParameter(), Throws.TypeOf<IncompleteModelException>());

// Single IFeature wired via the public API → returned.
var owningType = new Type();
var feature = new Feature();

owningType.AssignOwnership(parameterMembership, feature);

Assert.That(parameterMembership.ComputeOwnedMemberParameter(), Is.SameAs(feature));

// Wiring two features to verify the multiple-element guard:
// First remove the existing wiring so we can create a fresh membership with two elements.
var twoElementMembership = new ParameterMembership();
// Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var twoFeatureMembership = new ParameterMembership();
var secondFeature = new Feature();

((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(feature);
((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(secondFeature);
((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(feature);
((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature);

Assert.That(() => twoElementMembership.ComputeOwnedMemberParameter(), Throws.TypeOf<IncompleteModelException>());
Assert.That(() => twoFeatureMembership.ComputeOwnedMemberParameter(), Throws.TypeOf<IncompleteModelException>());

// NOTE: wiring a non-IFeature element as the sole OwnedRelatedElement is not possible via the
// public AssignOwnership API (IParameterMembership requires an IFeature target).
// To cover the as-cast-returns-null path we directly populate OwnedRelatedElement with a
// plain Namespace (which is not an IFeature).
// Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace).
// The OfType<IFeature>() projection MUST pick out the IFeature regardless of its position
// (this is the core robustness guarantee — never positionally index the unfiltered collection).
var mixedMembership = new ParameterMembership();
var siblingNonFeature = new Namespace();
var mixedFeature = new Feature();

((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonFeature);
((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedFeature);

Assert.That(mixedMembership.ComputeOwnedMemberParameter(), Is.SameAs(mixedFeature));

// OwnedRelatedElement populated with non-IFeature element(s) only → no IFeature match:
// [1..1] violation, throws IncompleteModelException.
var nonFeatureMembership = new ParameterMembership();
var nonFeatureElement = new Namespace();

((IContainedRelationship)nonFeatureMembership).OwnedRelatedElement.Add(nonFeatureElement);

Assert.That(nonFeatureMembership.ComputeOwnedMemberParameter(), Is.Null);
Assert.That(() => nonFeatureMembership.ComputeOwnedMemberParameter(), Throws.TypeOf<IncompleteModelException>());
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,15 @@ public void VerifyComputeSubjectParameter()

Assert.That(requirementDefinition.ComputeSubjectParameter(), Is.Null);

// Populated case: SubjectMembership is present; selecting ownedSubjectParameter triggers an
// upstream stub (SubjectMembershipExtensions.ComputeOwnedSubjectParameter is not yet implemented).
// Populated case: SubjectMembership is present alongside the earlier ParameterMembership.
// OfType<ISubjectMembership> must discriminate — only the subject's ownedSubjectParameter surfaces.
// This also covers the mixed-state discrimination: both a ParameterMembership and a SubjectMembership
// are wired; the result must be the subject usage, not the parameter usage.
var subjectMembership = new SubjectMembership();
var subjectUsage = new Usage();
requirementDefinition.AssignOwnership(subjectMembership, subjectUsage);

Assert.That(() => requirementDefinition.ComputeSubjectParameter(), Throws.TypeOf<NotSupportedException>());
Assert.That(requirementDefinition.ComputeSubjectParameter(), Is.SameAs(subjectUsage));
}

private static readonly string[] ExpectedSingleComputedText = ["The requirement text."];
Expand Down
12 changes: 7 additions & 5 deletions SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,17 +229,19 @@ public void VerifyComputeSubjectParameter()

Assert.That(requirementUsage.ComputeSubjectParameter(), Is.Null);

// Populated case: SubjectMembership is present; selecting ownedSubjectParameter triggers an
// upstream stub (SubjectMembershipExtensions.ComputeOwnedSubjectParameter is not yet implemented).
// Populated case: SubjectMembership is present alongside the earlier ParameterMembership.
// OfType<ISubjectMembership> must discriminate — only the subject's ownedSubjectParameter surfaces.
// This also covers the mixed-state discrimination: both a ParameterMembership and a SubjectMembership
// are wired; the result must be the subject usage, not the parameter usage.
var subjectMembership = new SubjectMembership();
var subjectUsage = new Usage();
requirementUsage.AssignOwnership(subjectMembership, subjectUsage);

Assert.That(() => requirementUsage.ComputeSubjectParameter(), Throws.TypeOf<NotSupportedException>());
Assert.That(requirementUsage.ComputeSubjectParameter(), Is.SameAs(subjectUsage));
}

private static readonly string[] ExpectedSingleComputedText = new[] { "The requirement text." };
private static readonly string[] ExpectedMultipleComputedText = new[] { "The requirement text.", "Additional context." };
private static readonly string[] ExpectedSingleComputedText = ["The requirement text."];
private static readonly string[] ExpectedMultipleComputedText = ["The requirement text.", "Additional context."];

[Test]
public void VerifyComputeText()
Expand Down
Loading
Loading