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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

namespace SysML2.NET.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators
{
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -103,5 +104,80 @@ public async Task Verify_that_ExtentExpression_case_carries_synthesised_guard()
"Synthesised guard for ExtentExpression must include the parsed `ownedRelationship += TypeReferenceMember` cursor predicate.");
});
}

/// <summary>
/// Regression for the selective nested-alternation flattening fix. A metaclass reachable only
/// THROUGH a nested alternation rule used to be captured by an earlier arm typed on one of its
/// supertypes, because the depth sort ranks a nested-rule arm by the RULE's declared target
/// (<c>BehaviorUsageElement : Usage</c>, shallow) rather than by the deepest metaclass reachable
/// through it (<c>PerformActionUsage</c>, deep) — and forces that arm last as <c>default:</c> when
/// the target IS the generating class. So <c>BuildVariantUsageElement</c> matched every
/// <c>IPerformActionUsage</c> against <c>case IEventOccurrenceUsage</c> (a supertype per
/// <c>IPerformActionUsage : IActionUsage, IEventOccurrenceUsage</c>) and emitted
/// <c>variant event doX;</c> instead of <c>variant perform doX;</c>. The fix hoists one
/// <c>case</c> arm per genuinely-shadowed class to the top of the switch, delegating to the nested
/// rule's builder. This test pins both known instances — <c>VariantUsageElement</c> and
/// <c>OwnedRelatedElement</c> (where <c>IMultiplicity : IFeature</c> was swallowed by
/// <c>case IFeature</c> before reaching <c>NonFeatureElement</c>).
/// </summary>
[Test]
public async Task Verify_that_shadowed_nested_alternation_targets_are_hoisted()
{
await this.umlCoreTextualNotationBuilderGenerator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.textualNotationSpecification, this.umlPocoDirectoryInfo);

var generatedUsageBuilderPath = Path.Combine(this.umlPocoDirectoryInfo.FullName, "UsageTextualNotationBuilder.cs");
var generatedElementBuilderPath = Path.Combine(this.umlPocoDirectoryInfo.FullName, "ElementTextualNotationBuilder.cs");

Assert.That(File.Exists(generatedUsageBuilderPath), Is.True, $"Expected generator to emit {generatedUsageBuilderPath}");
Assert.That(File.Exists(generatedElementBuilderPath), Is.True, $"Expected generator to emit {generatedElementBuilderPath}");

var buildVariantUsageElement = ExtractMethodBody(await File.ReadAllTextAsync(generatedUsageBuilderPath), "BuildVariantUsageElement");
var buildOwnedRelatedElement = ExtractMethodBody(await File.ReadAllTextAsync(generatedElementBuilderPath), "BuildOwnedRelatedElement");

var performActionArmIndex = buildVariantUsageElement.IndexOf("case SysML2.NET.Core.POCO.Systems.Actions.IPerformActionUsage pocoPerformActionUsage:", StringComparison.Ordinal);
var eventOccurrenceArmIndex = buildVariantUsageElement.IndexOf("case SysML2.NET.Core.POCO.Systems.Occurrences.IEventOccurrenceUsage pocoEventOccurrenceUsage:", StringComparison.Ordinal);
var multiplicityArmIndex = buildOwnedRelatedElement.IndexOf("case SysML2.NET.Core.POCO.Core.Types.IMultiplicity pocoMultiplicity:", StringComparison.Ordinal);
var featureArmIndex = buildOwnedRelatedElement.IndexOf("case SysML2.NET.Core.POCO.Core.Features.IFeature pocoFeature:", StringComparison.Ordinal);

using (Assert.EnterMultipleScope())
{
Assert.That(performActionArmIndex, Is.GreaterThanOrEqualTo(0),
"BuildVariantUsageElement must hoist an IPerformActionUsage arm — PerformActionUsage is only reachable through the nested BehaviorUsageElement rule.");
Assert.That(performActionArmIndex, Is.LessThan(eventOccurrenceArmIndex),
"The hoisted IPerformActionUsage arm must precede case IEventOccurrenceUsage, otherwise the supertype arm swallows it and `variant perform` renders as `variant event`.");
Assert.That(buildVariantUsageElement, Does.Contain("BuildBehaviorUsageElement(pocoPerformActionUsage, writerContext, stringBuilder);"),
"The hoisted arm must delegate to the nested rule's own builder so no builder is bypassed.");
Assert.That(buildVariantUsageElement, Does.Contain("case SysML2.NET.Core.POCO.Systems.States.IExhibitStateUsage pocoExhibitStateUsage:"),
"ExhibitStateUsage (IExhibitStateUsage : IStateUsage, IPerformActionUsage) is shadowed by the same IEventOccurrenceUsage arm and must be hoisted too.");
Assert.That(buildVariantUsageElement, Does.Contain("case SysML2.NET.Core.POCO.Systems.UseCases.IIncludeUseCaseUsage pocoIncludeUseCaseUsage:"),
"IncludeUseCaseUsage (IIncludeUseCaseUsage : IUseCaseUsage, IPerformActionUsage) is shadowed by the same IEventOccurrenceUsage arm and must be hoisted too.");
Assert.That(multiplicityArmIndex, Is.GreaterThanOrEqualTo(0),
"BuildOwnedRelatedElement must hoist an IMultiplicity arm — Multiplicity is only reachable through the nested NonFeatureElement rule.");
Assert.That(multiplicityArmIndex, Is.LessThan(featureArmIndex),
"The hoisted IMultiplicity arm must precede case IFeature — Multiplicity is a NonFeatureElement alternative but IMultiplicity : IFeature.");
Assert.That(buildOwnedRelatedElement, Does.Contain("BuildNonFeatureElement(pocoMultiplicity, writerContext, stringBuilder);"),
"The hoisted IMultiplicity arm must delegate to BuildNonFeatureElement, the rule that lists Multiplicity as an alternative.");
}
}

/// <summary>
/// Extracts the source of a single generated builder method, so arm-ordering assertions anchor on
/// the method under test rather than on the first file-wide match of a case label.
/// </summary>
/// <param name="generatedSource">The full generated builder source</param>
/// <param name="methodName">The name of the <c>public static void Build…</c> method to extract</param>
/// <returns>The method's source, up to the start of the next method</returns>
private static string ExtractMethodBody(string generatedSource, string methodName)
{
var methodStartIndex = generatedSource.IndexOf($"public static void {methodName}(", StringComparison.Ordinal);

Assert.That(methodStartIndex, Is.GreaterThanOrEqualTo(0), $"Expected the generated source to declare {methodName}");

var nextMethodIndex = generatedSource.IndexOf("public static void ", methodStartIndex + 1, StringComparison.Ordinal);

return nextMethodIndex < 0
? generatedSource[methodStartIndex..]
: generatedSource[methodStartIndex..nextMethodIndex];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,46 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC
writer.WriteSafeString(Environment.NewLine);
}

/// <summary>
/// Emits the fall-through arm of a repeated group's per-element <c>switch</c>. When the group has
/// bare non-terminal alternatives (dispatcher rules over the same cursor, e.g. <c>TypeBodyElement</c>
/// in <c>( TypeBodyElement | ownedRelationship += ReturnFeatureMember )*</c>) the arm delegates to
/// them; those rules advance the cursor themselves, so no <c>Move()</c> is emitted here per the
/// Golden Rule. With no dispatcher alternative the arm just advances, which is what terminates the
/// loop on an element the group cannot render.
/// </summary>
/// <param name="writer">The <see cref="EncodedTextWriter" /> to emit to</param>
/// <param name="umlClass">The rule's target <see cref="IClass" /></param>
/// <param name="dispatcherNonTerminals">The group's bare (non-assignment) non-terminal alternatives</param>
/// <param name="cursorVariableName">The cursor driving the repeated group</param>
/// <param name="ruleGenerationContext">The current <see cref="RuleGenerationContext" /></param>
private static void EmitCollectionGroupFallThrough(EncodedTextWriter writer, IClass umlClass, List<NonTerminalElement> dispatcherNonTerminals, string cursorVariableName, RuleGenerationContext ruleGenerationContext)
{
var dispatcherCalls = dispatcherNonTerminals
.Select(nonTerminal =>
{
var referencedRule = ruleGenerationContext.FindRule(nonTerminal.Name);

return referencedRule?.EffectiveTarget == null
? null
: ResolveBuilderCall(umlClass, nonTerminal, referencedRule.EffectiveTarget, ruleGenerationContext);
})
.Where(call => call != null)
.ToList();

if (dispatcherCalls.Count == 0)
{
writer.WriteSafeString($"{cursorVariableName}.Move();{Environment.NewLine}");

return;
}

foreach (var dispatcherCall in dispatcherCalls)
{
writer.WriteSafeString($"{dispatcherCall}{Environment.NewLine}");
}
}

/// <summary>
/// Resolves the type condition for a collection while loop.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@
.OfType<NonTerminalElement>()
.ToList();

// Alternatives of the repeated group that are a BARE non-terminal rather than a
// `+=` assignment (e.g. `TypeBodyElement` in
// `( TypeBodyElement | ownedRelationship += ReturnFeatureMember )*`). Such a rule is a
// per-item dispatcher over the SAME cursor that advances the cursor itself, so it
// becomes the loop's fall-through arm — without it the group's switch has no case for
// those elements and the trailing `Move()` silently discards them.
var groupDispatcherNonTerminals = groupElement.Alternatives
.SelectMany(alternative => alternative.Elements)
.OfType<NonTerminalElement>()
.Where(nonTerminal => nonTerminal.Container is not AssignmentElement)
.ToList();

if (groupAssignments.Count > 0 && groupNonTerminals.Count == groupAssignments.Count)
{
var groupPropertyName = groupAssignments[0].Property;
Expand Down Expand Up @@ -174,7 +186,18 @@

var groupOrderedElements = RuleQueryUtilities.OrderElementsByInheritance(groupNonTerminals, umlClass.Cache, ruleGenerationContext);

writer.WriteSafeString($"while ({groupCursorVarName}.Current != null){Environment.NewLine}");
// A `*` group must not swallow an element that a FOLLOWING sibling consumes
// from the same cursor — `( … )* ( ownedRelationship += ResultExpressionMember )?`
// left the trailing optional facing an exhausted cursor and dropped the result
// expression. Exclude the next sibling's target type from the loop condition.
var groupWhileCondition = this.ResolveCollectionWhileTypeCondition(groupCursorVarName, umlClass, null, groupPropertyName, ruleGenerationContext);

if (string.IsNullOrWhiteSpace(groupWhileCondition))
{
groupWhileCondition = $"{groupCursorVarName}.Current != null";
}

writer.WriteSafeString($"while ({groupWhileCondition}){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
writer.WriteSafeString($"switch ({groupCursorVarName}.Current){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
Expand All @@ -192,16 +215,21 @@
ruleGenerationContext.CurrentVariableName = previousVariableName;
ruleGenerationContext.CallerRule = previousCaller;

writer.WriteSafeString($"{Environment.NewLine}break;{Environment.NewLine}");
// The case body built the element itself, so this `+=` consumption owns the
// cursor advance (Golden Rule) — the fall-through arm below must NOT repeat it.
writer.WriteSafeString($"{Environment.NewLine}{groupCursorVarName}.Move();{Environment.NewLine}break;{Environment.NewLine}");
}

writer.WriteSafeString($"default:{Environment.NewLine}");
EmitCollectionGroupFallThrough(writer, umlClass, groupDispatcherNonTerminals, groupCursorVarName, ruleGenerationContext);
writer.WriteSafeString($"break;{Environment.NewLine}");

writer.WriteSafeString($"}}{Environment.NewLine}");
writer.WriteSafeString($"{groupCursorVarName}.Move();{Environment.NewLine}");
writer.WriteSafeString($"}}{Environment.NewLine}");
}
else
{
var handCodedRuleName = groupElement.TextualNotationRule?.RuleName ?? "Unknown";

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

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'Unknown' 6 times.
EmitHandCodedFallback(writer, handCodedRuleName, ruleGenerationContext);
}
}
Expand Down Expand Up @@ -235,7 +263,7 @@

if (!ruleGenerationContext.IsNextElementNewLineTerminal())
{
writer.WriteSafeString("stringBuilder.Append(' ');");

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

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'stringBuilder.Append(' ');' 5 times.
}
}
else
Expand Down
Loading
Loading