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
2 changes: 1 addition & 1 deletion .github/workflows/CodeQuality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
run: dotnet build --no-restore --no-incremental /p:ContinuousIntegrationBuild=true

- name: Run Tests and Compute Coverage
run: dotnet-coverage collect "dotnet test SysML2.NET.sln --no-restore --no-build --verbosity normal" -f xml -o "coverage.xml"
run: dotnet-coverage collect "dotnet test SysML2.NET.sln --no-restore --no-build --verbosity normal --filter TestCategory!=Integration" -f xml -o "coverage.xml"

- name: Sonarqube end
env:
Expand Down
3 changes: 2 additions & 1 deletion SySML2.NET.REST.Tests/RestClientTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ namespace SySML2.NET.REST.Tests
using NUnit.Framework;

/// <summary>
/// Suite of tests for the <see cref="RestClient"/> class
/// Suite of tests for the <see cref="RestClient"/> class.
/// </summary>
[TestFixture]
[Category("Integration")]
public class RestClientTestFixture
{
private string baseUri;
Expand Down
93 changes: 93 additions & 0 deletions SysML2.NET.CodeGenerator/GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,96 @@ dotnet test SysML2.NET.sln
```bash
grep -r "HandCoded" SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/*.cs | wc -l
```

## Known KEBNF / specification divergences

The `.kebnf` files are OMG-owned and **never edited**. Where a production cannot describe the
notation the pilot implementation actually reads and writes, the writer deviates and the deviation is
recorded here. All of these are reported upstream in
[SysML-v2-Release issue #124](https://github.com/Systems-Modeling/SysML-v2-Release/issues/124)
("Several textual KEBNF productions appear unreachable or inconsistent with release examples").

Do **not** "fix" the writer back to the literal production without checking this table first.

### #124 item 8 — `EntryTransitionMember` emits a duplicated `then` (deviation implemented)

```
EntryTransitionMember : FeatureMembership =
MemberPrefix ( ownedRelatedElement += GuardedTargetSuccession
| 'then' ownedRelatedElement += TargetSuccession ) ';'

TargetSuccession : SuccessionAsUsage =
ownedRelationship += SourceEndMember 'then' ownedRelationship += ConnectorEndMember
```

`TargetSuccession` supplies its own `'then'`, so the literal reading of the second alternative is
`then then S1;`. The corpus writes `entry; then off;`
(`Validation/05-State-based Behavior/5-State-based Behavior-2.sysml`), and the pilot's Xtext uses
`TransitionSuccession` (`EmptySourceEndMember ConnectorEndMember`, no `'then'`) at
`org.omg.sysml.xtext/src/org/omg/sysml/xtext/SysML.xtext:1798`.

Confirmed against both independent sources of the grammar — the `.kebnf` and the OMG specification
(SysML 2.0 §8.2.2.18.1 State Definitions, §8.2.2.17.8 Action Successions) — so this is a genuine
specification defect, not a transcription slip.

The model cannot discriminate the two productions: both are a `SuccessionAsUsage` with two
`EndFeatureMembership`s, and the multiplicity present on the source end is added by the pilot's
transform to *both* ends. **Deviation:** `BuildEntryTransitionMemberHandCoded` suppresses the rule's
own `'then'` and lets `TargetSuccession` supply it. The structure the KEBNF specifies is still
honoured; only the redundant keyword is dropped.

### #124 item 7 — `end` prefix on non-reference usages (deviation accepted, not implemented)

`DefaultReferenceUsage` has no `EndUsagePrefix`, so `end ref hitch` / `end port p1: P;` are
unreachable, yet appear in the corpus (`03-Function-based Behavior/3c-…-1`, `3c-…-2`). The writer
emits the pilot's form; the difference is recorded as an accepted deviation for those files.

### Items expected to affect folders not yet validated

Not yet investigated — listed so the cause is recognised on first encounter rather than
re-diagnosed:

| item | production | folder likely affected |
|---|---|---|
| #1 | `AllocationDefinition` missing from `DefinitionElement` | 12-Dependency Relationships |
| #3 | `MetadataUsage` not wired into any dispatch point | 14-Language Extensions |
| #9 | `SatisfyRequirementUsage` requires `assert` | 08-Requirements |
| #10 | `CaseBodyItem` admits no `ReturnParameterMember` | 10-Analysis and Trades |
| #11 | `EnumeratedValue` cannot carry prefix metadata (`#Security enum secret`) | 13-Model Containment, 14-Language Extensions |

Items #2, #4, #5, #6 concern productions with no corpus coverage.

## Model ↔ notation reconciliations (NOT divergences)

Cases where the grammar offers two conformant productions for one model, so the writer must choose.
Nothing here deviates from the specification — unlike the divergences above.

### `TargetTransitionUsage` — the implied transition source

```
StateBodyItem : Type = …
| ( ownedRelationship += SourceSuccessionMember )?
ownedRelationship += BehaviorUsageMember
( ownedRelationship += TargetTransitionUsageMember )* ← shorthand
| ownedRelationship += TransitionUsageMember ← explicit
```

`state off; accept X then Y;` and `transition off accept X then Y;` are **both normative** and produce
the *same* model: the pilot resolves the shorthand at parse time and stores the source explicitly as a
`FeatureChainMember` (a non-owning `Membership` cross-referencing the state). The shorthand-ness is
therefore not recoverable, and `TargetTransitionUsage` has **no notation for the source at all**.

The writer prefers the shorthand (matching the corpus) only when all three hold, each required for
correctness rather than style:

- the transition is **anonymous** — `TargetTransitionUsage` has no `UsageDeclaration` slot, so a named
transition would silently lose its name (this is what keeps `5-…-1` / `5-…-1a` on the explicit form);
- its source **is** the anchor feature of the preceding `BehaviorUsageMember` — otherwise the shorthand
re-parses against that state and denotes a different element;
- it is positioned in the `( … )*` run following that member.

Consequence, in `TypeTextualNotationBuilder.EmitTargetTransitionRun`: the transition's own
`ownedRelationship` cursor is advanced once **past the source with no emission**. That is a deliberate
exception to the `Move()` ↔ `+=` Golden Rule, valid because the elected production has no notation for
that element. It is conditional — it only runs after `QueryImpliedSourceTransition` has confirmed
position 0 is the source membership — so it cannot consume a real element.
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC
}
}

// A guarded body-item rule (IsGuardedBodyItemRule) admits elements that have no
// notation, and its per-item builder refuses to consume them WITHOUT advancing the
// cursor. The loop must therefore test the same predicate as the enclosing entry
// guard: a bare non-null test spins forever on the first refused element.
if (IsGuardedBodyItemRule(nonTerminalElement.Name))
{
var guardVariableName = $"{targetProperty.Name.LowerCaseFirstLetter()}BodyItem";

whileCondition = $"{cursorVariableName}.Current is SysML2.NET.Core.POCO.Root.Elements.IRelationship {guardVariableName} && {guardVariableName}.IsValidFor{nonTerminalElement.Name}(writerContext)";
}

if (perItemCall != null)
{
writer.WriteSafeString($"while ({whileCondition}){Environment.NewLine}");
Expand Down
106 changes: 103 additions & 3 deletions SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using SysML2.NET.CodeGenerator.Grammar.Model;

using uml4net.Extensions;
using uml4net.SimpleClassifiers;
using uml4net.StructuredClassifiers;

/// <summary>
Expand Down Expand Up @@ -266,7 +267,51 @@
}

var targetClass = RuleQueryUtilities.FindClass(umlClass.Cache, typeTarget);
return targetClass?.QueryFullyQualifiedTypeName();
var targetTypeName = targetClass?.QueryFullyQualifiedTypeName();

if (targetTypeName == null)
{
return null;
}

// A rule may PIN a property to a constant through a non-parsing assignment, e.g.
// `GuardExpressionMember : TransitionFeatureMembership = 'if' { kind = 'guard' } …`.
// Sibling rules then share one target type and are distinguishable ONLY by that constant,
// so it has to be part of the guard or the first sibling swallows them all.
var pinnedConstantPattern = ResolvePinnedConstantPattern(referencedRule, targetClass);

return pinnedConstantPattern == null ? targetTypeName : $"{targetTypeName} {pinnedConstantPattern}";
}

/// <summary>
/// Builds a C# property pattern for a constant a rule pins via a non-parsing assignment
/// (<c>{ kind = 'guard' }</c>), used to tell apart sibling rules that share a target type.
/// </summary>
/// <param name="referencedRule">The rule whose pinned constant is sought.</param>
/// <param name="targetClass">The rule's target <see cref="IClass" />.</param>
/// <returns>The property pattern, or <see langword="null" /> when nothing enum-typed is pinned.</returns>
private static string ResolvePinnedConstantPattern(TextualNotationRule referencedRule, IClass targetClass)
{
var pinnedAssignment = referencedRule.Alternatives
.SelectMany(alternative => alternative.Elements)
.OfType<NonParsingAssignmentElement>()
.FirstOrDefault(assignment => assignment.Operator == "=" && !string.IsNullOrWhiteSpace(assignment.Value));

if (pinnedAssignment == null)
{
return null;
}

var property = targetClass.QueryAllProperties()
.FirstOrDefault(x => string.Equals(x.Name, pinnedAssignment.PropertyName, StringComparison.OrdinalIgnoreCase));

if (property?.Type is not IEnumeration)
{
return null;
}

var literalName = pinnedAssignment.Value.Trim('\'').CapitalizeFirstLetter();
return $"{{ {property.Name.CapitalizeFirstLetter()}: {property.Type.QueryFullyQualifiedTypeName()}.{literalName} }}";
}

/// <summary>
Expand Down Expand Up @@ -447,7 +492,13 @@
}
}

if (inlineConditionParts.Count > 0)
var optionalCollectionCondition = TryResolveOptionalCollectionGroupCondition(umlClass, elements, ruleGenerationContext);

if (optionalCollectionCondition != null)
{
writer.WriteSafeString($"{Environment.NewLine}if ({optionalCollectionCondition}){Environment.NewLine}");
}
else if (inlineConditionParts.Count > 0)
{
writer.WriteSafeString($"{Environment.NewLine}if ({string.Join(" || ", inlineConditionParts)}){Environment.NewLine}");
}
Expand All @@ -474,6 +525,54 @@
}
}

/// <summary>
/// Resolves the guard for an optional group whose only variable content is a <c>*</c>-quantified
/// bare non-terminal — e.g. <c>( '{' ActionBodyItem* '}' )?</c>. Such a group must be emitted only
/// when its loop would iterate at least once: the group's own terminals carry no information, so a
/// property-based condition wrongly emits an empty <c>{ }</c> whenever any unrelated property is set.
/// </summary>
/// <param name="umlClass">The related <see cref="IClass" /></param>
/// <param name="elements">The optional group's elements</param>
/// <param name="ruleGenerationContext">The current <see cref="RuleGenerationContext" /></param>
/// <returns>The cursor-based condition, or <see langword="null" /> when the group is not that shape.</returns>
private static string TryResolveOptionalCollectionGroupCondition(IClass umlClass, List<RuleElement> elements, RuleGenerationContext ruleGenerationContext)
{
var nonTerminals = elements.OfType<NonTerminalElement>().ToList();

if (nonTerminals.Count != 1 || !nonTerminals[0].IsCollection || elements.Any(element => element is AssignmentElement or GroupElement))
{
return null;
}

var referencedRule = ruleGenerationContext.FindRule(nonTerminals[0].Name);
var collectionPropertyNames = referencedRule?.QueryCollectionPropertyNames(ruleGenerationContext.AllRules);

if (collectionPropertyNames?.Count != 1)
{
return null;
}

var targetProperty = umlClass.QueryAllProperties().SingleOrDefault(x => string.Equals(x.Name, collectionPropertyNames.Single(), StringComparison.OrdinalIgnoreCase));

if (targetProperty == null || !targetProperty.QueryIsEnumerable())
{
return null;
}

// The cursor is declared up-front by DeclareAllRequiredCursors; if it is absent this is not the
// shape we handle, so fall back rather than emit a second declaration.
var existingCursor = ruleGenerationContext.DefinedCursors.SingleOrDefault(x => x.IsCursorValidForProperty(targetProperty));

if (existingCursor == null)
{
return null;
}

return IsGuardedBodyItemRule(nonTerminals[0].Name)
? $"{existingCursor.CursorVariableName}.Current is SysML2.NET.Core.POCO.Root.Elements.IRelationship optionalBodyCandidate && optionalBodyCandidate.IsValidFor{nonTerminals[0].Name}(writerContext)"
: $"{existingCursor.CursorVariableName}.Current != null";
}

/// <summary>
/// Processes multiple alternatives where every alternative has exactly one element.
/// Handles multi-collection assignments, unityped dispatch, and mixed-type element handling.
Expand Down Expand Up @@ -1010,7 +1109,7 @@
writer.WriteSafeString($"{subclass.Name}TextualNotationBuilder.Build{nonTerminalElement.Name}({patternVariableName}, writerContext, stringBuilder);{Environment.NewLine}");
writer.WriteSafeString($"}}{Environment.NewLine}");

// A NonTerminal-valued assignment emits its own null guard inside ProcessAssignmentElement;

Check warning on line 1112 in SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.

Check warning on line 1112 in SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.
// only a value-literal assignment (e.g. [QualifiedName]) needs the guard supplied here.
if (assignmentElement.Value is ValueLiteralElement)
{
Expand Down Expand Up @@ -1275,7 +1374,7 @@

foreach (var element in secondAlt.Elements)
{
if (element is NonTerminalElement { IsCollection: true })

Check warning on line 1377 in SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this redundant cast.
{
var cursorVarName = $"{targetProperty.Name.LowerCaseFirstLetter()}Cursor";
writer.WriteSafeString($"var {cursorVarName} = writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName});{Environment.NewLine}");
Expand Down Expand Up @@ -1414,7 +1513,8 @@
private static bool IsGuardedBodyItemRule(string bodyItemRuleName)
{
return string.Equals(bodyItemRuleName, "DefinitionBodyItem", StringComparison.Ordinal)
|| string.Equals(bodyItemRuleName, "InterfaceBodyItem", StringComparison.Ordinal);
|| string.Equals(bodyItemRuleName, "InterfaceBodyItem", StringComparison.Ordinal)
|| string.Equals(bodyItemRuleName, "ActionBodyItem", StringComparison.Ordinal);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ package '1a-Parts Tree' {
/*
* 'frontAxleAssembly' is a nested part of part 'vehicle1'.
* It is a composite part of the containing part.
*
* (And similarly for 'rearAxleAssembly'.)
*/
part frontAxle: Axle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ package '2a-Parts Interconnection' {
/*
* The two rear wheels of 'rearAxleAssembly' must be given
* their own names in order to be referenced in connections.
*
* (":>" is a shorthand here for "subsets".)
*/
part leftWheel :> rearWheel = rearWheel#(1) {
Expand Down
Loading
Loading