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
56 changes: 56 additions & 0 deletions SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,62 @@ public static string QueryIfStatementContentForNonEmpty(this IProperty property,
return "THIS WILL PRODUCE COMPILE ERROR";
}

/// <summary>
/// Finds the minimal set of subclasses of <paramref name="rootClass"/> (including <paramref name="rootClass"/>
/// itself) that <b>directly</b> redefine <paramref name="property"/> with a <c>&lt;defaultValue&gt;</c>
/// equal to <paramref name="literalTriggerValue"/>. Used by the textual-notation codegen to suppress
/// emission of a <c>?= 'literal'</c> keyword when the property's runtime value matches the
/// metamodel default for the concrete subtype.
/// <para>"Minimal" means: if a class C is in the set and any of C's ancestors is also in the set, C
/// is removed (the C# <c>is</c> check on the ancestor already matches C at runtime).</para>
/// <para>Comparison of <paramref name="literalTriggerValue"/> to the property's <c>defaultValue</c> is
/// done via <see cref="QueryDefaultValueAsString"/>, which already normalises booleans
/// (<c>"true"</c> / <c>"false"</c>), enum literals, integers, and strings.</para>
/// </summary>
/// <param name="property">The base property whose redefinitions are being scanned (e.g. <c>Usage::isReference</c>).</param>
/// <param name="rootClass">The metaclass under which to scan (typically the textual rule's effective target).</param>
/// <param name="literalTriggerValue">The default-value string that should trigger inclusion in the exclusion set (e.g. <c>"true"</c>).</param>
/// <returns>The minimal list of redefining classes; empty when no subclass redefines with the matching default.</returns>
public static IReadOnlyList<IClass> QuerySubclassesWithMatchingDefault(this IProperty property, IClass rootClass, string literalTriggerValue)
{
ArgumentNullException.ThrowIfNull(property);
ArgumentNullException.ThrowIfNull(rootClass);
ArgumentException.ThrowIfNullOrWhiteSpace(literalTriggerValue);

var allClasses = rootClass.Cache.Values.OfType<IClass>();
var introducers = new List<IClass>();

foreach (var candidateClass in allClasses)
{
if (candidateClass != rootClass && !candidateClass.QueryAllGeneralClassifiers().Contains(rootClass))
{
continue;
}

foreach (var ownedAttribute in candidateClass.OwnedAttribute)
{
if (!string.Equals(ownedAttribute.Name, property.Name, StringComparison.Ordinal))
{
continue;
}

var defaultValueAsString = ownedAttribute.QueryDefaultValueAsString();

if (string.Equals(defaultValueAsString, literalTriggerValue, StringComparison.OrdinalIgnoreCase))
{
introducers.Add(candidateClass);
break;
}
}
}

// Reduce to the minimal set: drop any class whose ancestor is already present, because the
// C# `poco is not IAncestor` check at runtime already covers the descendant.
return introducers
.Where(candidate => !introducers.Any(other => other != candidate && candidate.QueryAllGeneralClassifiers().Contains(other)))
.ToList();
}

/// <summary>
/// Returns every <see cref="IConstraint"/> from the owning class's <c>OwnedRule</c> that
/// applies to the given derived <see cref="IProperty"/>. The XMI shipped with this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
}
else
{
var handCodedRuleName = groupElement.TextualNotationRule?.RuleName ?? "Unknown";

Check warning on line 200 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 @@ -231,7 +231,7 @@

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

Check warning on line 234 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 Expand Up @@ -374,7 +374,19 @@
{
if (!isPartOfMultipleAlternative && assignmentElement.Container is not GroupElement { IsOptional: true })
{
writer.WriteSafeString($"if({targetProperty.QueryIfStatementContentForNonEmpty("poco")}){Environment.NewLine}");
// KEBNF `Prop ?= 'literal'` — emit the literal when the runtime
// value is truthy, but suppress it for concrete subtypes whose
// metamodel default already equals the literal-trigger value.
// For those subtypes the keyword is structurally redundant and
// the canonical idiomatic source omits it (see e.g. SysML
// `attribute X` rather than `ref attribute X` because
// AttributeUsage's `isReference` default is `true`).
var exclusionTypes = targetProperty.QuerySubclassesWithMatchingDefault(umlClass, "true");
var exclusionClause = exclusionTypes.Count == 0
? string.Empty
: $" && poco is not ({string.Join(" or ", exclusionTypes.Select(c => c.QueryFullyQualifiedTypeName()))})";

writer.WriteSafeString($"if({targetProperty.QueryIfStatementContentForNonEmpty("poco")}{exclusionClause}){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
writer.WriteSafeString($"stringBuilder.Append(\" {terminalElement.Value} \");{Environment.NewLine}");
writer.WriteSafeString('}');
Expand Down
Loading
Loading