Skip to content

Commit 57947a6

Browse files
Fix #152 (#239)
* Fix #152 * Correct call to operation directly * claude.md update * update extension-impl.md
1 parent 08d6ae1 commit 57947a6

6 files changed

Lines changed: 182 additions & 47 deletions

File tree

.claude/team-templates/extension-impl.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,27 @@ hand-reformat of `FeatureExtensions.cs`. All four roles must enforce these.
187187
- **Use camelCase derived properties when in scope and implemented**: prefer
188188
`subject.fooBar` over re-deriving when that derived property is itself
189189
implemented.
190+
- **Invoke operations and derived properties via the POCO instance member,
191+
not the static `Compute*` extension**e.g.
192+
`subject.IsDistinguishableFrom(other)`, `subject.qualifiedName`,
193+
`subject.NamingFeature()`, NOT
194+
`MembershipExtensions.ComputeIsDistinguishableFromOperation(subject, other)`,
195+
`ElementExtensions.ComputeQualifiedName(subject)`,
196+
`FeatureExtensions.ComputeNamingFeatureOperation(subject)`. The POCO's
197+
instance member dispatches virtually and honors any subclass
198+
**redefinition** of the operation/derived property; calling the static
199+
extension directly bypasses dispatch and silently skips overridesa
200+
real defect class, easy to introduce and hard to spot.
201+
**Exception (oclAsType pattern):** when the OCL itself uses
202+
`self.oclAsType(SuperType).method()`, the C# translation MUST use the
203+
static-extension-method form to BYPASS dispatch and target the SuperType's
204+
body. Two precedents:
205+
- `Usage::namingFeature()` → `FeatureExtensions.ComputeNamingFeatureOperation(usage)`
206+
(would otherwise recurse into the Usage override).
207+
- `OwningMembership::path()` → `RelationshipExtensions.ComputeRedefinedPathOperation(owningMembership)`
208+
(same shape, OwningMembership upcast to Relationship).
209+
Without an explicit `oclAsType(...)` in the source OCL, default to the
210+
POCO instance call.
190211
- **Do not change the namespace, using directives, or method signatures.**
191212

192213
---

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,4 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package
159159
- Use 'NotSupportedException' (not 'NotImplementedException') for placeholder/stub methods that require manual implementation
160160
- 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
161161
- 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
162+
- 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)`)

SysML2.NET.Tests/Extend/MembershipExtensionsTestFixture.cs

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,108 @@
2121
namespace SysML2.NET.Tests.Extend
2222
{
2323
using System;
24-
24+
2525
using NUnit.Framework;
26-
26+
2727
using SysML2.NET.Core.POCO.Root.Namespaces;
28+
using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage;
2829

2930
[TestFixture]
3031
public class MembershipExtensionsTestFixture
3132
{
3233
[Test]
33-
public void ComputeMemberElementId_ThrowsNotSupportedException()
34+
public void VerifyComputeMemberElementId()
3435
{
35-
Assert.That(() => ((IMembership)null).ComputeMemberElementId(), Throws.TypeOf<NotSupportedException>());
36+
Assert.That(() => ((IMembership)null).ComputeMemberElementId(), Throws.TypeOf<ArgumentNullException>());
37+
38+
var membership = new Membership();
39+
var element = new Definition { ElementId = "test-element-id-42" };
40+
membership.MemberElement = element;
41+
42+
Assert.That(membership.ComputeMemberElementId(), Is.EqualTo("test-element-id-42"));
43+
}
44+
45+
[Test]
46+
public void VerifyComputeIsDistinguishableFromOperation()
47+
{
48+
// Null guards
49+
Assert.That(() => ((IMembership)null).ComputeIsDistinguishableFromOperation(new Membership()), Throws.TypeOf<ArgumentNullException>());
50+
51+
Assert.That(() => new Membership().ComputeIsDistinguishableFromOperation(null), Throws.TypeOf<ArgumentNullException>());
52+
53+
// Clause C: incompatible metaclasses — types do not conform to each other → true
54+
var subjectIncompat = new Membership { MemberShortName = "A", MemberName = "A" };
55+
subjectIncompat.MemberElement = new Definition();
56+
57+
var otherIncompat = new Membership { MemberShortName = "A", MemberName = "A" };
58+
otherIncompat.MemberElement = new Namespace();
59+
60+
Assert.That(subjectIncompat.ComputeIsDistinguishableFromOperation(otherIncompat), Is.True);
61+
62+
// Clause C edge case: null MemberElement on subject — no conformance possible → true
63+
var subjectNullElement = new Membership { MemberShortName = "A", MemberName = "A" };
64+
subjectNullElement.MemberElement = null;
65+
66+
var otherWithElement = new Membership { MemberShortName = "A", MemberName = "A" };
67+
otherWithElement.MemberElement = new Definition();
68+
69+
Assert.That(subjectNullElement.ComputeIsDistinguishableFromOperation(otherWithElement), Is.True);
70+
71+
// Clause C edge case: null MemberElement on other — no conformance possible → true
72+
var subjectWithElement = new Membership { MemberShortName = "A", MemberName = "A" };
73+
subjectWithElement.MemberElement = new Definition();
74+
75+
var otherNullElement = new Membership { MemberShortName = "A", MemberName = "A" };
76+
otherNullElement.MemberElement = null;
77+
78+
Assert.That(subjectWithElement.ComputeIsDistinguishableFromOperation(otherNullElement), Is.True);
79+
80+
// Clause B: same metaclass, all four cross-comparisons differ → true
81+
var subjectClauseB = new Membership { MemberShortName = "A", MemberName = "B" };
82+
subjectClauseB.MemberElement = new Definition();
83+
84+
var otherClauseB = new Membership { MemberShortName = "X", MemberName = "Y" };
85+
otherClauseB.MemberElement = new Definition();
86+
87+
Assert.That(subjectClauseB.ComputeIsDistinguishableFromOperation(otherClauseB), Is.True);
88+
89+
// Clause A: both MemberShortName and MemberName null on subject → true (null name-part wins)
90+
var subjectClauseA = new Membership { MemberShortName = null, MemberName = null };
91+
subjectClauseA.MemberElement = new Definition();
92+
93+
var otherClauseA = new Membership { MemberShortName = "X", MemberName = "Y" };
94+
otherClauseA.MemberElement = new Definition();
95+
96+
Assert.That(subjectClauseA.ComputeIsDistinguishableFromOperation(otherClauseA), Is.True);
97+
98+
// Indistinguishable: same metaclass + MemberShortName clash → false
99+
var subjectShortClash = new Membership { MemberShortName = "A", MemberName = "B" };
100+
subjectShortClash.MemberElement = new Definition();
101+
102+
var otherShortClash = new Membership { MemberShortName = "A", MemberName = "Z" };
103+
otherShortClash.MemberElement = new Definition();
104+
105+
Assert.That(subjectShortClash.ComputeIsDistinguishableFromOperation(otherShortClash), Is.False);
106+
107+
// Indistinguishable: subject.MemberShortName matches other.MemberName → false
108+
var subjectCrossClash = new Membership { MemberShortName = "A", MemberName = "B" };
109+
subjectCrossClash.MemberElement = new Definition();
110+
111+
var otherCrossClash = new Membership { MemberShortName = null, MemberName = "A" };
112+
otherCrossClash.MemberElement = new Definition();
113+
114+
Assert.That(subjectCrossClash.ComputeIsDistinguishableFromOperation(otherCrossClash), Is.False);
115+
116+
// Half-distinguishable: NamePart1 fires but NamePart2 fails (MemberName matches) → false
117+
var subjectHalf = new Membership { MemberShortName = "A", MemberName = "B" };
118+
subjectHalf.MemberElement = new Definition();
119+
120+
var otherHalf = new Membership { MemberShortName = "X", MemberName = "B" };
121+
otherHalf.MemberElement = new Definition();
122+
123+
Assert.That(subjectHalf.ComputeIsDistinguishableFromOperation(otherHalf), Is.False);
36124
}
37-
125+
38126
[Test]
39127
public void ComputeMembershipOwningNamespace_ThrowsNotSupportedException()
40128
{

SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,28 @@ public void VerifyComputeImportedMembershipsOperation()
240240
var ownedMembership = new OwningMembership { Visibility = VisibilityKind.Public };
241241
namespaceElement.AssignOwnership(ownedMembership, collidingElement);
242242

243+
// Clause B (cross-comparisons): import and owned share both metaclass (Definition)
244+
// and MemberName ("imported"), so they are NOT distinguishable -> import excluded.
243245
Assert.That(namespaceElement.ComputeImportedMembershipsOperation([]), Has.Count.EqualTo(0));
246+
247+
// Clause C (metaclass non-conformance): wire a second importedNamespace whose
248+
// owned member is a Namespace (NOT a Definition) named "imported". It collides on
249+
// MemberName with the owned Definition above, but the metaclasses are unrelated
250+
// (Namespace vs Definition — neither IsAssignableFrom the other), so per
251+
// Membership::isDistinguishableFrom Clause C the pair IS distinguishable, and the
252+
// import must surface. The previous partial helper omitted Clause C and would have
253+
// wrongly excluded this import; this assertion locks in the spec-correct behavior.
254+
var crossMetaclassNamespace = new Namespace();
255+
var crossMetaclassElement = new Namespace { DeclaredName = "imported" };
256+
var crossMetaclassMembership = new OwningMembership { Visibility = VisibilityKind.Public };
257+
crossMetaclassNamespace.AssignOwnership(crossMetaclassMembership, crossMetaclassElement);
258+
259+
var crossMetaclassImport = new NamespaceImport { ImportedNamespace = crossMetaclassNamespace };
260+
namespaceElement.AssignOwnership(crossMetaclassImport);
261+
262+
Assert.That(
263+
namespaceElement.ComputeImportedMembershipsOperation([]),
264+
Is.EquivalentTo([crossMetaclassMembership]));
244265
}
245266

246267
[Test]

SysML2.NET/Extend/MembershipExtensions.cs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ internal static class MembershipExtensions
4242
/// <returns>
4343
/// the computed result
4444
/// </returns>
45-
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
4645
internal static string ComputeMemberElementId(this IMembership membershipSubject)
4746
{
48-
throw new NotSupportedException("Create a GitHub issue when this method is required");
47+
return membershipSubject == null
48+
? throw new ArgumentNullException(nameof(membershipSubject))
49+
: membershipSubject.MemberElement.ElementId;
4950
}
5051

5152
/// <summary>
@@ -78,10 +79,48 @@ internal static INamespace ComputeMembershipOwningNamespace(this IMembership mem
7879
/// <returns>
7980
/// The expected <see cref="bool" />
8081
/// </returns>
81-
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
8282
internal static bool ComputeIsDistinguishableFromOperation(this IMembership membershipSubject, IMembership other)
8383
{
84-
throw new NotSupportedException("Create a GitHub issue when this method is required");
84+
if (membershipSubject == null)
85+
{
86+
throw new ArgumentNullException(nameof(membershipSubject));
87+
}
88+
89+
if (other == null)
90+
{
91+
throw new ArgumentNullException(nameof(other));
92+
}
93+
94+
// Clause C: metaclass incompatibility.
95+
// OCL: not (memberElement.oclKindOf(other.memberElement.oclType())
96+
// or other.memberElement.oclKindOf(memberElement.oclType()))
97+
// De Morgan: !A && !B. A null memberElement on either side trips this
98+
// (no conformance is possible).
99+
var thisType = membershipSubject.MemberElement?.GetType();
100+
var otherType = other.MemberElement?.GetType();
101+
102+
if (thisType == null || otherType == null
103+
|| (!otherType.IsAssignableFrom(thisType)
104+
&& !thisType.IsAssignableFrom(otherType)))
105+
{
106+
return true;
107+
}
108+
109+
// NamePart1 (OCL spells it shortMemberName — known XMI typo, real
110+
// attribute is MemberShortName):
111+
// memberShortName = null
112+
// OR (memberShortName != other.memberShortName
113+
// AND memberShortName != other.memberName)
114+
var shortNamePart = string.IsNullOrWhiteSpace(membershipSubject.MemberShortName)
115+
|| (membershipSubject.MemberShortName != other.MemberShortName
116+
&& membershipSubject.MemberShortName != other.MemberName);
117+
118+
// NamePart2: same shape, MemberName variant.
119+
var namePart = string.IsNullOrWhiteSpace(membershipSubject.MemberName)
120+
|| (membershipSubject.MemberName != other.MemberShortName
121+
&& membershipSubject.MemberName != other.MemberName);
122+
123+
return shortNamePart && namePart;
85124
}
86125
}
87126
}

SysML2.NET/Extend/NamespaceExtensions.cs

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -368,49 +368,14 @@ internal static List<IMembership> ComputeImportedMembershipsOperation(this IName
368368

369369
var ownedMemberships = namespaceSubject.ownedMembership;
370370

371-
return
371+
return
372372
[
373373
..importedMemberships.Where(import =>
374-
ownedMemberships.All(owned => IsDistinguishableMembership(import, owned))
375-
&& importedMemberships.All(other => other == import || IsDistinguishableMembership(import, other)))
374+
ownedMemberships.All(import.IsDistinguishableFrom)
375+
&& importedMemberships.All(other => other == import || import.IsDistinguishableFrom(other)))
376376
];
377377
}
378378

379-
/// <summary>
380-
/// Determines whether <paramref name="left"/> is distinguishable from <paramref name="right"/>
381-
/// according to the default OCL body of <c>Membership::isDistinguishableFrom</c>.
382-
/// </summary>
383-
/// <remarks>
384-
/// OCL (KerML XMI):
385-
/// <code>
386-
/// memberShortName = null and memberName = null or
387-
/// (memberShortName &lt;&gt; other.memberShortName and memberShortName &lt;&gt; other.memberName and
388-
/// memberName &lt;&gt; other.memberShortName and memberName &lt;&gt; other.memberName)
389-
/// </code>
390-
/// </remarks>
391-
/// <param name="left">
392-
/// The <see cref="IMembership" /> on whose perspective distinguishability is evaluated.
393-
/// </param>
394-
/// <param name="right">
395-
/// The <see cref="IMembership" /> being compared against.
396-
/// </param>
397-
/// <returns>
398-
/// <see langword="true"/> when <paramref name="left"/> can be distinguished from <paramref name="right"/>
399-
/// per the default Membership distinguishability rule; otherwise <see langword="false"/>.
400-
/// </returns>
401-
private static bool IsDistinguishableMembership(IMembership left, IMembership right)
402-
{
403-
if (left.MemberShortName == null && left.MemberName == null)
404-
{
405-
return true;
406-
}
407-
408-
return left.MemberShortName != right.MemberShortName
409-
&& left.MemberShortName != right.MemberName
410-
&& left.MemberName != right.MemberShortName
411-
&& left.MemberName != right.MemberName;
412-
}
413-
414379
/// <summary>
415380
/// If visibility is not null, return the Memberships of this Namespace with the given visibility,
416381
/// including ownedMemberships with the given visibility and Memberships imported with the given

0 commit comments

Comments
 (0)