Skip to content

Commit 875a904

Browse files
[Add] remarks into XML doc of extension method to capture defined constraints (#229)
Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
1 parent dfde5ca commit 875a904

116 files changed

Lines changed: 2731 additions & 76 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
namespace SysML2.NET.CodeGenerator.Extensions
2222
{
2323
using System;
24+
using System.Collections.Generic;
2425
using System.Linq;
2526

2627
using uml4net.Classification;
28+
using uml4net.CommonStructure;
2729
using uml4net.Extensions;
2830
using uml4net.SimpleClassifiers;
31+
using uml4net.StructuredClassifiers;
2932

3033
/// <summary>
3134
/// Extension class for the <see cref="IProperty"/>
@@ -136,5 +139,51 @@ public static string QueryIfStatementContentForNonEmpty(this IProperty property,
136139

137140
return "THIS WILL PRODUCE COMPILE ERROR";
138141
}
142+
143+
/// <summary>
144+
/// Returns every <see cref="IConstraint"/> from the owning class's <c>OwnedRule</c> that
145+
/// applies to the given derived <see cref="IProperty"/>. The XMI shipped with this
146+
/// project links derivation rules to the owning class (not the specific attribute) and uses
147+
/// the naming convention <c>derive{ClassName}{PropertyName}</c>; both signals are honoured.
148+
/// </summary>
149+
/// <param name="property">The property to query. Must not be <see langword="null"/>.</param>
150+
/// <returns>An enumerable of constraining <see cref="IConstraint"/>s; empty when none apply.</returns>
151+
public static IEnumerable<IConstraint> QueryOwnedConstraints(this IProperty property)
152+
{
153+
ArgumentNullException.ThrowIfNull(property);
154+
155+
if (property.Owner is not IClass owningClass)
156+
{
157+
return [];
158+
}
159+
160+
var explicitMatches = owningClass.OwnedRule
161+
.Where(rule => rule.ConstrainedElement.Contains(property))
162+
.ToList();
163+
164+
if (explicitMatches.Count > 0)
165+
{
166+
return explicitMatches;
167+
}
168+
169+
var expectedDeriveName = "derive" + owningClass.Name + property.Name.CapitalizeFirstLetter();
170+
171+
return owningClass.OwnedRule
172+
.Where(rule => string.Equals(rule.Name, expectedDeriveName, StringComparison.Ordinal));
173+
}
174+
175+
/// <summary>
176+
/// Returns every <see cref="IConstraint"/> directly owned by the given <see cref="IOperation"/>
177+
/// via its <c>OwnedRule</c> namespace facet. Used by the Extend code-generation template to
178+
/// surface the operation's constraint(s) as XML <c>&lt;remarks&gt;</c> blocks.
179+
/// </summary>
180+
/// <param name="operation">The operation to query. Must not be <see langword="null"/>.</param>
181+
/// <returns>An enumerable of <see cref="IConstraint"/>s; empty when none are declared.</returns>
182+
public static IEnumerable<IConstraint> QueryOwnedConstraints(this IOperation operation)
183+
{
184+
ArgumentNullException.ThrowIfNull(operation);
185+
186+
return operation.OwnedRule;
187+
}
139188
}
140189
}

SysML2.NET.CodeGenerator/HandleBarHelpers/PropertyHelper.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
namespace SysML2.NET.CodeGenerator.HandleBarHelpers
2222
{
2323
using System;
24+
using System.Collections.Generic;
2425
using System.Globalization;
2526
using System.Linq;
2627
using System.Text;
@@ -36,6 +37,7 @@ namespace SysML2.NET.CodeGenerator.HandleBarHelpers
3637
using uml4net.Classification;
3738
using uml4net.CommonStructure;
3839
using uml4net.StructuredClassifiers;
40+
using uml4net.Values;
3941

4042
/// <summary>
4143
/// A handlebars block helper for the <see cref="IProperty"/> interface
@@ -1120,6 +1122,151 @@ public static void RegisterPropertyHelper(this IHandlebars handlebars)
11201122

11211123
return property.Type is IClassifier { IsAbstract: true };
11221124
});
1125+
1126+
handlebars.RegisterHelper("Property.QueryHasOwnedConstraints", (_, arguments) =>
1127+
{
1128+
if (arguments.Length != 1 || arguments[0] is not IProperty property)
1129+
{
1130+
throw new HandlebarsException("{{Property.QueryHasOwnedConstraints}} expects a single IProperty argument");
1131+
}
1132+
1133+
return HasRenderableConstraint(property.QueryOwnedConstraints());
1134+
});
1135+
1136+
handlebars.RegisterHelper("Operation.QueryHasOwnedConstraints", (_, arguments) =>
1137+
{
1138+
if (arguments.Length != 1 || arguments[0] is not IOperation operation)
1139+
{
1140+
throw new HandlebarsException("{{Operation.QueryHasOwnedConstraints}} expects a single IOperation argument");
1141+
}
1142+
1143+
return HasRenderableConstraint(operation.QueryOwnedConstraints());
1144+
});
1145+
1146+
handlebars.RegisterHelper("Property.WriteOwnedRulesAsRemarksBlock", (writer, _, arguments) =>
1147+
{
1148+
if (arguments.Length != 1 || arguments[0] is not IProperty property)
1149+
{
1150+
throw new HandlebarsException("{{Property.WriteOwnedRulesAsRemarksBlock}} expects a single IProperty argument");
1151+
}
1152+
1153+
writer.WriteSafeString(BuildOwnedRulesRemarksBlock(property.QueryOwnedConstraints()));
1154+
});
1155+
1156+
handlebars.RegisterHelper("Operation.WriteOwnedRulesAsRemarksBlock", (writer, _, arguments) =>
1157+
{
1158+
if (arguments.Length != 1 || arguments[0] is not IOperation operation)
1159+
{
1160+
throw new HandlebarsException("{{Operation.WriteOwnedRulesAsRemarksBlock}} expects a single IOperation argument");
1161+
}
1162+
1163+
writer.WriteSafeString(BuildOwnedRulesRemarksBlock(operation.QueryOwnedConstraints()));
1164+
});
1165+
}
1166+
1167+
/// <summary>
1168+
/// Builds a complete XML <c>&lt;remarks&gt;</c> block listing every constraint body carried by
1169+
/// the supplied <see cref="IConstraint"/> sequence, labelled by language. Returns the empty
1170+
/// string when no constraint carries an <see cref="IOpaqueExpression"/> body.
1171+
/// </summary>
1172+
/// <param name="constraints">The constraints to render.</param>
1173+
/// <returns>
1174+
/// A multi-line string starting with <c>/// &lt;remarks&gt;</c> and ending with <c>/// &lt;/remarks&gt;</c>,
1175+
/// each line prefixed for direct emission inside the Extend template; empty when nothing to emit.
1176+
/// </returns>
1177+
/// <summary>
1178+
/// Returns <see langword="true"/> when at least one constraint in <paramref name="constraints"/>
1179+
/// carries an <see cref="IOpaqueExpression"/> with at least one non-blank body line — i.e. when
1180+
/// <see cref="BuildOwnedRulesRemarksBlock"/> would emit a non-empty <c>&lt;remarks&gt;</c> block.
1181+
/// </summary>
1182+
/// <param name="constraints">The constraints to evaluate.</param>
1183+
/// <returns>Whether the helpers would produce any output for these constraints.</returns>
1184+
private static bool HasRenderableConstraint(IEnumerable<IConstraint> constraints)
1185+
{
1186+
foreach (var constraint in constraints)
1187+
{
1188+
var opaqueExpression = constraint.Specification?.OfType<IOpaqueExpression>().FirstOrDefault();
1189+
1190+
if (opaqueExpression?.Body != null && opaqueExpression.Body.Any(body => !string.IsNullOrWhiteSpace(body)))
1191+
{
1192+
return true;
1193+
}
1194+
}
1195+
1196+
return false;
1197+
}
1198+
1199+
private static string BuildOwnedRulesRemarksBlock(IEnumerable<IConstraint> constraints)
1200+
{
1201+
var entries = new List<(string Language, string Body)>();
1202+
1203+
foreach (var constraint in constraints)
1204+
{
1205+
var opaqueExpression = constraint.Specification?.OfType<IOpaqueExpression>().FirstOrDefault();
1206+
1207+
if (opaqueExpression == null || opaqueExpression.Body == null)
1208+
{
1209+
continue;
1210+
}
1211+
1212+
var bodies = opaqueExpression.Body;
1213+
var languages = opaqueExpression.Language;
1214+
1215+
for (var index = 0; index < bodies.Count; index++)
1216+
{
1217+
if (string.IsNullOrWhiteSpace(bodies[index]))
1218+
{
1219+
continue;
1220+
}
1221+
1222+
var language = languages != null && index < languages.Count && !string.IsNullOrWhiteSpace(languages[index])
1223+
? languages[index].Trim()
1224+
: "Constraint";
1225+
1226+
entries.Add((language, bodies[index].Trim()));
1227+
}
1228+
}
1229+
1230+
if (entries.Count == 0)
1231+
{
1232+
return string.Empty;
1233+
}
1234+
1235+
var sb = new StringBuilder();
1236+
sb.AppendLine("/// <remarks>");
1237+
1238+
for (var index = 0; index < entries.Count; index++)
1239+
{
1240+
var (language, body) = entries[index];
1241+
1242+
sb.AppendLine($"/// {EscapeXml(language)}:");
1243+
sb.AppendLine("/// <code>");
1244+
1245+
foreach (var line in body.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'))
1246+
{
1247+
sb.AppendLine($"/// {EscapeXml(line.TrimEnd())}");
1248+
}
1249+
1250+
sb.AppendLine("/// </code>");
1251+
}
1252+
1253+
sb.Append("/// </remarks>");
1254+
1255+
return sb.ToString();
1256+
}
1257+
1258+
/// <summary>
1259+
/// Replaces XML-significant characters with their entity equivalents so the result is safe to
1260+
/// embed inside an XML doc comment.
1261+
/// </summary>
1262+
/// <param name="value">The string to escape.</param>
1263+
/// <returns>The escaped string.</returns>
1264+
private static string EscapeXml(string value)
1265+
{
1266+
return value
1267+
.Replace("&", "&amp;")
1268+
.Replace("<", "&lt;")
1269+
.Replace(">", "&gt;");
11231270
}
11241271

11251272
/// <summary>

SysML2.NET.CodeGenerator/Templates/Uml/core-poco-extend-uml-template.hbs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
2323
using System;
2424
using System.Collections.Generic;
2525
26-
{{ #Class.WriteEnumerationNameSpaces this}}
26+
{{ #Class.WriteEnumerationNameSpacesWithOperation this}}
2727
{{ #Class.WriteNameSpaces this POCO}}
2828
2929
/// <summary>
@@ -37,6 +37,9 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
3737
/// <summary>
3838
/// Computes the derived property.
3939
/// </summary>
40+
{{#if (Property.QueryHasOwnedConstraints property)}}
41+
{{Property.WriteOwnedRulesAsRemarksBlock property}}
42+
{{/if}}
4043
/// <param name="{{String.LowerCaseFirstLetter ../this.Name}}Subject">
4144
/// The subject <see cref="I{{../this.Name}}"/>
4245
/// </param>
@@ -53,6 +56,9 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
5356
{{/each}}
5457
{{#each (this.OwnedOperation) as | operation | }}
5558
{{ #Documentation operation }}
59+
{{#if (Operation.QueryHasOwnedConstraints operation)}}
60+
{{Operation.WriteOwnedRulesAsRemarksBlock operation}}
61+
{{/if}}
5662
/// <param name="{{String.LowerCaseFirstLetter ../this.Name}}Subject">
5763
/// The subject <see cref="I{{../this.Name}}"/>
5864
/// </param>
@@ -63,7 +69,7 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
6369
throw new NotSupportedException("Create a GitHub issue when this method is required");
6470
}
6571
{{#unless @last}}
66-
72+
6773
{{/unless}}
6874
{{/each}}
6975
}

SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ public void VerifyComputeVisibleMembershipsOperation()
217217
[Test]
218218
public void VerifyComputeImportedMembershipsOperation()
219219
{
220+
Assert.That(() => ((INamespace)null).ComputeImportedMembershipsOperation([]), Throws.TypeOf<ArgumentNullException>());
221+
220222
var namespaceElement = new Namespace();
221223

222224
Assert.That(namespaceElement.ComputeImportedMembershipsOperation([]), Has.Count.EqualTo(0));

SysML2.NET.Tests/Extend/OwningMembershipExtensionsTestFixture.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,53 @@
2121
namespace SysML2.NET.Tests.Extend
2222
{
2323
using System;
24-
24+
2525
using NUnit.Framework;
26-
26+
27+
using SysML2.NET.Core.POCO.Root.Elements;
2728
using SysML2.NET.Core.POCO.Root.Namespaces;
29+
using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage;
30+
using SysML2.NET.Exceptions;
31+
using SysML2.NET.Extensions;
2832

2933
[TestFixture]
3034
public class OwningMembershipExtensionsTestFixture
3135
{
3236
[Test]
33-
public void ComputeOwnedMemberElement_ThrowsArgumentNullException()
37+
public void VerifyComputeOwnedMemberElement()
3438
{
3539
Assert.That(() => ((IOwningMembership)null).ComputeOwnedMemberElement(), Throws.TypeOf<ArgumentNullException>());
40+
41+
// OwnedRelatedElement.Count == 0 → IncompleteModelException
42+
var emptyMembership = new OwningMembership();
43+
Assert.That(() => emptyMembership.ComputeOwnedMemberElement(), Throws.TypeOf<IncompleteModelException>());
44+
45+
// OwnedRelatedElement.Count == 1 → returns that element
46+
var container = new Namespace();
47+
var singleMembership = new OwningMembership();
48+
var ownedElement = new Definition { DeclaredName = "SingleElement" };
49+
container.AssignOwnership(singleMembership, ownedElement);
50+
Assert.That(singleMembership.ComputeOwnedMemberElement(), Is.SameAs(ownedElement));
51+
52+
// OwnedRelatedElement.Count > 1 → IncompleteModelException
53+
var multiMembership = new OwningMembership();
54+
((IContainedRelationship)multiMembership).OwnedRelatedElement.Add(new Definition());
55+
((IContainedRelationship)multiMembership).OwnedRelatedElement.Add(new Definition());
56+
Assert.That(() => multiMembership.ComputeOwnedMemberElement(), Throws.TypeOf<IncompleteModelException>());
3657
}
37-
58+
3859
[Test]
3960
public void ComputeOwnedMemberElementId_ThrowsNotSupportedException()
4061
{
4162
Assert.That(() => ((IOwningMembership)null).ComputeOwnedMemberElementId(), Throws.TypeOf<NotSupportedException>());
4263
}
43-
64+
4465
[Test]
4566
public void ComputeOwnedMemberName_ThrowsArgumentNullException()
4667
{
4768
Assert.That(() => ((IOwningMembership)null).ComputeOwnedMemberName(), Throws.TypeOf<ArgumentNullException>());
4869
}
49-
70+
5071
[Test]
5172
public void ComputeOwnedMemberShortName_ThrowsArgumentNullException()
5273
{

0 commit comments

Comments
 (0)