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
38 changes: 30 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,28 +194,50 @@ Direct pushes to `development` or `master` are forbidden. All work lives on a fe
- 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
- **Always use C# auto-properties** (`public T Foo { get; private set; }`, `public T Foo { get; init; }`, `public T Foo { get; }`) — NEVER pair a private backing field with an expression-bodied or full-getter property when there is no non-trivial logic (validation, normalisation, lazy init, event firing). Mere storage is never a justification for a backing field; the compiler collapses auto-properties to the same IL.
- **For test fixtures: default to ONE `[Test]` method per class / method-under-test** packing every scenario (happy path, edge cases, null guards, alternate inputs) into multiple `Assert.That` calls inside that one test — per `TESTING.md` §2. Do NOT write one `[Test]` per scenario when the setup is shared; that produces a bloated test list and duplicated arrange boilerplate. Split into separate `[Test]` methods only when each scenario has a genuinely distinct, complex setup.
- **Prefer method-group syntax over lambda when the lambda merely invokes a no-arg method.** Both in production code and in tests, write `Assert.That(subject.ComputeFoo, Throws.TypeOf<X>())` rather than `Assert.That(() => subject.ComputeFoo(), Throws.TypeOf<X>())`; pass `subject.Handle` rather than `x => subject.Handle(x)` when wiring up an event handler; pass `string.IsNullOrWhiteSpace` rather than `s => string.IsNullOrWhiteSpace(s)` to a LINQ predicate. The method group is more concise, allocates no closure, and reads as the action itself rather than as a delegate that calls the action. Fall back to a lambda only when (a) the lambda's body does more than the bare call (transforms args, captures locals, adds null-handling), (b) the target method is overloaded and the compiler can't infer which overload to bind, or (c) the call needs explicit type arguments the method-group form cannot supply.
- 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**. For the common case of a `[1..1]` derived property, use the canonical shared helper `ElementExtensions.RequireSingleOfType<T>(this IReadOnlyList<IElement>, string)` from `SysML2.NET/Extensions/ElementExtensions.cs` — it does a zero-allocation index-based scan, early-exits on the second match, and throws `IncompleteModelException` with distinct "missing" vs "more than one" diagnostics:
- **`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**. Two canonical shared helpers in `SysML2.NET/Extensions/ElementExtensions.cs` cover the common cases (all early-exit on the second match, no full materialisation), each with two overloads:

- **`SingleStrict<T>`** — `[1..1]` semantics. Empty → `throw IncompleteModelException` (lower-bound violation, missing required). Single → return. 2+ → `throw MultiplicityViolationException` (upper-bound violation).
- `SingleStrict<T>(this IEnumerable<T>, string)` — homogeneous: source is already typed `T`.
- `SingleStrict<TResult>(this IEnumerable, string)` — heterogeneous: source is wider; bundles a `OfType<TResult>()` filter before the strict-single check.
- **`SingleOrDefaultStrict<T>`** — `[0..1]` semantics. Empty → `return null`. Single → return. 2+ → `throw MultiplicityViolationException`.
- `SingleOrDefaultStrict<T>(this IEnumerable<T>, string)` — homogeneous.
- `SingleOrDefaultStrict<TResult>(this IEnumerable, string)` — heterogeneous with implicit `OfType<TResult>()`.

```csharp
// [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage)
return subject.OwnedRelatedElement.RequireSingleOfType<ITargetType>(nameof(subject));
return subject.OwnedRelatedElement.SingleStrict<ITargetType>(nameof(subject));

// [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement)
return subject.OwnedRelatedElement.RequireSingleOfType<IElement>(nameof(subject));
return subject.OwnedRelatedElement.SingleStrict<IElement>(nameof(subject));

// [0..1] type-narrowed projection over a storage collection (e.g. ConstraintUsage::constraintDefinition : IPredicate)
return subject.type.SingleOrDefaultStrict<IPredicate>(nameof(subject));

// [0..1] over an already-projected stream (multi-hop chain ending in a Select/predicate)
return subject.OwnedRelationship.OfType<ISomeRelationship>().Select(r => r.something).SingleOrDefaultStrict(nameof(subject));
```

The helper signature is `internal static T RequireSingleOfType<T>(this IReadOnlyList<IElement> elements, string subjectName) where T : class, IElement`. Because `IReadOnlyList<T>` is covariant, the same helper works on `IElement.OwnedRelationship` (whose element type is `IRelationship : IElement`) without an additional overload.
All four signatures accept `IEnumerable` / `IEnumerable<T>`; storage collections (`IElement.OwnedRelationship`, `IRelationship.OwnedRelatedElement`) and derived enumerables (e.g. `Feature::type`) bind directly — no `IReadOnlyList` overload needed.

The failure mode the helper produces matches the **derived property's declared multiplicity** as recorded in the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface (or in the UML XMI):
The failure mode each helper produces matches the **derived property's declared multiplicity** as recorded in the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface (or in the UML XMI):

| 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` (do NOT use the helper — write inline) | return the match | `throw IncompleteModelException` (inline) |
| `[1..1]` (lowerValue=1, upperValue=1) | `throw IncompleteModelException` (lower-bound violation, missing required) | return the match | `throw MultiplicityViolationException` (upper-bound violation) |
| `[0..1]` (lowerValue=0, upperValue=1) | `return null` (use `SingleOrDefaultStrict<TResult>` for direct `OfType<TResult>` over a storage collection or derived enumerable; chain explicit projections then `SingleOrDefaultStrict()` for multi-hop / predicate-filtered) | return the match | `throw MultiplicityViolationException` |
| `[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).
Strictness applies **only to the final filter** of the projection chain. Intermediate filters (e.g. `OwnedRelationship.OfType<IFeatureTyping>()` in a `FeatureTyping → Type → IXxxDefinition` chain) may legitimately match many elements; the upper-bound check applies to the last `.OfType<T>()` whose result is the `[0..1]` / `[1..1]` derived value.

**OCL gate (overrides the table for `[0..1]`):** when the OCL derivation body in the property's `<remarks><code>` block explicitly elects the first of many (`->first()`, `->at(1)`), the spec contract is "pick the first if multiple" — keep `FirstOrDefault`. The strict `[0..1]` rule applies only when the OCL has no first-picking call (or no OCL body at all), in which case the multiplicity `[0..1]` is the only contract and 2+ must surface as `MultiplicityViolationException`.

`IncompleteModelException` and `MultiplicityViolationException` are the loud signals to SDK users that the model is malformed:
- `IncompleteModelException` — lower-bound violation: the model is missing a required element (0 matches against a `[1..1]` property).
- `MultiplicityViolationException` — upper-bound violation: the model carries more elements than the upper bound allows (2+ matches against a `[0..1]` or `[1..1]` property).

DO NOT swallow them as `null` when the multiplicity demands a throw, and DO NOT raise them for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property).

Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern (a) silently drops the correctly-typed element when it does not sit at index 0 (`AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`, so a Membership can carry annotation targets alongside the member element), and (b) always allocates a `List<T>` via `OfType<T>().ToList()` even when the answer is decidable after the first two elements.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void VerifyComputeOwnedActorParameter()
((IContainedRelationship)twoPartMembership).OwnedRelatedElement.Add(firstPart);
((IContainedRelationship)twoPartMembership).OwnedRelatedElement.Add(secondPart);

Assert.That(() => twoPartMembership.ComputeOwnedActorParameter(), Throws.TypeOf<IncompleteModelException>());
Assert.That(twoPartMembership.ComputeOwnedActorParameter, Throws.TypeOf<MultiplicityViolationException>());

// Mixed: non-IPartUsage (Namespace) alongside a single IPartUsage — the type filter picks
// out the PartUsage regardless of its position.
Expand Down
16 changes: 2 additions & 14 deletions SysML2.NET.Tests/Extend/AnalysisCaseUsageExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace SysML2.NET.Tests.Extend
using SysML2.NET.Core.POCO.Kernel.Functions;
using SysML2.NET.Core.POCO.Systems.AnalysisCases;
using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

[TestFixture]
Expand All @@ -41,20 +42,7 @@ public void VerifyComputeAnalysisCaseDefinition()
var analysisCaseUsage = new AnalysisCaseUsage();

// Empty case: no FeatureTyping whose Type is an IAnalysisCaseDefinition → null.
Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition(), Is.Null);

// Negative case: FeatureTyping whose Type is a Usage (not IAnalysisCaseDefinition) — no match → null.
var nonDefinitionTyping = new FeatureTyping { Type = new Usage() };
analysisCaseUsage.AssignOwnership(nonDefinitionTyping);

Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition(), Is.Null);

// Populated case: FeatureTyping whose Type is an AnalysisCaseDefinition → returns the AnalysisCaseDefinition.
var analysisCaseDefinition = new AnalysisCaseDefinition();
var analysisCaseDefinitionTyping = new FeatureTyping { Type = analysisCaseDefinition };
analysisCaseUsage.AssignOwnership(analysisCaseDefinitionTyping);

Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition(), Is.SameAs(analysisCaseDefinition));
Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition, Throws.TypeOf<NotSupportedException>());
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace SysML2.NET.Tests.Extend

using SysML2.NET.Core.POCO.Core.Features;
using SysML2.NET.Core.POCO.Kernel.Functions;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

[TestFixture]
Expand Down Expand Up @@ -54,6 +55,13 @@ public void VerifyComputePredicate()
var predicateTyping = new FeatureTyping { Type = predicate };
predicateBooleanExpression.AssignOwnership(predicateTyping);
Assert.That(predicateBooleanExpression.ComputePredicate(), Is.SameAs(predicate));

// Two FeatureTypings whose Type is a Predicate → MultiplicityViolationException (upper-bound
// violation of the derived [0..1] property).
var twoPredicateBooleanExpression = new BooleanExpression();
twoPredicateBooleanExpression.AssignOwnership(new FeatureTyping { Type = new Predicate() });
twoPredicateBooleanExpression.AssignOwnership(new FeatureTyping { Type = new Predicate() });
Assert.That(() => twoPredicateBooleanExpression.ComputePredicate(), Throws.TypeOf<MultiplicityViolationException>());
}
}
}
24 changes: 2 additions & 22 deletions SysML2.NET.Tests/Extend/CalculationUsageExtensionsTestFixture.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.Core.Types;
using SysML2.NET.Core.POCO.Kernel.Functions;
using SysML2.NET.Core.POCO.Systems.Calculations;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

using Type = SysML2.NET.Core.POCO.Core.Types.Type;
Expand All @@ -44,28 +45,7 @@ public void VerifyComputeCalculationDefinition()
var emptyCalculationUsage = new CalculationUsage();

// No FeatureTyping entries → no IFunction type → null (property is [0..1]).
Assert.That(emptyCalculationUsage.ComputeCalculationDefinition(), Is.Null);

// A FeatureTyping pointing at a non-Function Type must not satisfy the IFunction filter.
var nonFunctionSubject = new CalculationUsage();
var nonFunctionType = new Type();
nonFunctionSubject.AssignOwnership(new FeatureTyping { Type = nonFunctionType });

Assert.That(nonFunctionSubject.ComputeCalculationDefinition(), Is.Null);

// A FeatureTyping pointing at a CalculationDefinition (which implements IFunction) must be returned.
var subject = new CalculationUsage();
var calculationDefinition = new CalculationDefinition();
subject.AssignOwnership(new FeatureTyping { Type = calculationDefinition });

Assert.That(subject.ComputeCalculationDefinition(), Is.SameAs(calculationDefinition));

// A FeatureTyping pointing at a kernel Function also satisfies the IFunction filter.
var functionSubject = new CalculationUsage();
var kernelFunction = new Function();
functionSubject.AssignOwnership(new FeatureTyping { Type = kernelFunction });

Assert.That(functionSubject.ComputeCalculationDefinition(), Is.SameAs(kernelFunction));
Assert.That(emptyCalculationUsage.ComputeCalculationDefinition, Throws.TypeOf<NotSupportedException>());
}

[Test]
Expand Down
16 changes: 2 additions & 14 deletions SysML2.NET.Tests/Extend/CaseUsageExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace SysML2.NET.Tests.Extend
using SysML2.NET.Core.POCO.Systems.Parts;
using SysML2.NET.Core.POCO.Systems.Requirements;
using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

[TestFixture]
Expand Down Expand Up @@ -67,20 +68,7 @@ public void VerifyComputeCaseDefinition()
var caseUsage = new CaseUsage();

// Empty case: no FeatureTyping whose Type is an ICaseDefinition → null.
Assert.That(caseUsage.ComputeCaseDefinition(), Is.Null);

// Negative case: FeatureTyping whose Type is a Usage (not ICaseDefinition) — no match → null.
var nonDefinitionTyping = new FeatureTyping { Type = new Usage() };
caseUsage.AssignOwnership(nonDefinitionTyping);

Assert.That(caseUsage.ComputeCaseDefinition(), Is.Null);

// Populated case: FeatureTyping whose Type is a CaseDefinition → returns the CaseDefinition.
var caseDefinition = new CaseDefinition();
var caseDefinitionTyping = new FeatureTyping { Type = caseDefinition };
caseUsage.AssignOwnership(caseDefinitionTyping);

Assert.That(caseUsage.ComputeCaseDefinition(), Is.SameAs(caseDefinition));
Assert.That(caseUsage.ComputeCaseDefinition, Throws.TypeOf<NotSupportedException>());
}

[Test]
Expand Down
16 changes: 8 additions & 8 deletions SysML2.NET.Tests/Extend/ConcernUsageExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// <copyright file="ConcernUsageExtensionsTestFixture.cs" company="Starion Group S.A.">
//
//
// 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.
//
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace SysML2.NET.Tests.Extend
{
using System;

using NUnit.Framework;

using SysML2.NET.Core.POCO.Systems.Requirements;

[TestFixture]
Expand Down
Loading
Loading