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: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ Direct pushes to `development` or `master` are forbidden. All work lives on a fe
- Use meaningful variable names instead of single-letter names in any context (e.g., 'charIndex' instead of 'i', 'currentChar' instead of 'c', 'element' instead of 'e')
- Use 'NotSupportedException' (not 'NotImplementedException') for placeholder/stub methods that require manual implementation
- 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.
- 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:
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 @@ -342,7 +342,13 @@
{
if (assignmentElement.Value is NonTerminalElement { Name: "REGULAR_COMMENT" })
{
writer.WriteSafeString($"SharedTextualNotationBuilder.AppendRegularComment(stringBuilder, poco.{targetPropertyName});");
// Documentation rule (`doc /* … */`) surrounds the comment with
// blank lines so doc blocks are visually separated from their
// owning members. Every other rule that assigns a REGULAR_COMMENT
// body (currently only `Comment`) renders adjacent to its
// neighbouring statements per the SST convention.
var surroundWithBlankLines = string.Equals(ruleGenerationContext.NamedElementToGenerate?.Name, "Documentation", StringComparison.Ordinal);
writer.WriteSafeString($"SharedTextualNotationBuilder.AppendRegularComment(stringBuilder, poco.{targetPropertyName}, surroundWithBlankLines: {(surroundWithBlankLines ? "true" : "false")});");
}
else if (assignmentElement.Value is NonTerminalElement { Name: "NAME" })
{
Expand Down
79 changes: 71 additions & 8 deletions SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@

ruleGenerationContext.AllRules.AddRange(allRules);

var isOperatorExpressionRule = IsOperatorExpressionRule(textualRule, umlClass);
var isOperatorExpressionRule = IsOperatorExpressionRule(umlClass);
var isOwnedExpressionRule = string.Equals(textualRule.RuleName, "OwnedExpression", StringComparison.Ordinal);
var isInlineBraceBodyRule = IsInlineBraceBodyRule(textualRule);

if (isOwnedExpressionRule)
{
Expand All @@ -109,8 +110,29 @@
writer.WriteSafeString("try" + Environment.NewLine + "{" + Environment.NewLine);
}

// Inline-brace-body rules — rules whose body alternative has the exact
// shape `'{' SingleNonTerminal '}'` with no quantifier and no `+=`
// accumulator — render their <c>{ … }</c> wrapper on a single line per
// the SST tutorial convention (e.g. constraint and expression bodies).
// The three rules that match in the KEBNF are
// <c>FunctionBody</c>, <c>ExpressionBody</c>, and <c>CalculationBody</c>;

Check warning on line 118 in SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.

Check warning on line 118 in SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.
// every other brace-bounded rule uses a <c>*</c>-quantified list and
// renders multi-line. The wrapping suppresses AppendLine newlines inside
// the rule body and re-terminates the logical line on exit so the next
// owning statement starts on its own line.
if (isInlineBraceBodyRule)
{
writer.WriteSafeString("stringBuilder.EnterInlineBlock();" + Environment.NewLine);
writer.WriteSafeString("try" + Environment.NewLine + "{" + Environment.NewLine);
}

processor.ProcessAlternatives(writer, umlClass, textualRule.Alternatives, ruleGenerationContext);

if (isInlineBraceBodyRule)
{
writer.WriteSafeString("}" + Environment.NewLine + "finally" + Environment.NewLine + "{" + Environment.NewLine + "stringBuilder.ExitInlineBlock();" + Environment.NewLine + "stringBuilder.AppendLine();" + Environment.NewLine + "}" + Environment.NewLine);
}

if (isOperatorExpressionRule)
{
writer.WriteSafeString("}" + Environment.NewLine + "finally" + Environment.NewLine + "{" + Environment.NewLine + "writerContext.OperatorContextStack.Pop();" + Environment.NewLine + "}" + Environment.NewLine);
Expand All @@ -130,27 +152,68 @@
/// <c>WriteRule</c> to wrap the generated builder body with a precedence-stack
/// push/pop so operand-rendering can decide on parens.
/// </summary>
/// <param name="rule">The textual notation rule being generated.</param>
/// <param name="umlClass">The rule's target <see cref="IClass"/>.</param>
/// <returns><c>true</c> when the target is <c>OperatorExpression</c> or a subclass.</returns>
private static bool IsOperatorExpressionRule(TextualNotationRule rule, IClass umlClass)
private static bool IsOperatorExpressionRule(IClass umlClass)
{
if (umlClass == null)
{
return false;
}

if (string.Equals(umlClass.Name, "OperatorExpression", StringComparison.Ordinal))
return string.Equals(umlClass.Name, "OperatorExpression", StringComparison.Ordinal) || umlClass.QueryAllGeneralClassifiers().Any(general => string.Equals(general.Name, "OperatorExpression", StringComparison.Ordinal));
}

/// <summary>
/// Determines whether <paramref name="rule"/> has any alternative of the exact shape
/// <c>'{' SingleNonTerminal '}'</c> with no quantifier and no <c>+=</c> accumulator
/// on the inner non-terminal. The KEBNF grammar uses this shape exclusively for
/// expression-body wrappers — <c>FunctionBody</c>, <c>ExpressionBody</c> and
/// <c>CalculationBody</c> — whose canonical SST rendering is a single inline line
/// <c>{ expr }</c>. Every other brace-bounded rule uses a <c>*</c>-quantified list
/// (e.g. <c>'{' PackageBodyElement* '}'</c>) and renders multi-line.
/// </summary>
/// <param name="rule">The textual notation rule being generated.</param>
/// <returns>
/// <c>true</c> when the rule contains the inline brace-body shape and therefore
/// needs its braced alternative wrapped with
/// <c>stringBuilder.EnterInlineBlock()</c> / <c>stringBuilder.ExitInlineBlock()</c>.
/// </returns>
private static bool IsInlineBraceBodyRule(TextualNotationRule rule)
{
if (rule == null)
{
return true;
return false;
}

foreach (var general in umlClass.QueryAllGeneralClassifiers())
foreach (var alternative in rule.Alternatives.Where(alternative => alternative.Elements.Count == 3))

Check warning on line 189 in SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Loop should be simplified by calling Select(alternative => alternative.Elements))

See more on https://sonarcloud.io/project/issues?id=STARIONGROUP_SysML2.NET&issues=AZ6RqieAKPEJmiI96hgp&open=AZ6RqieAKPEJmiI96hgp&pullRequest=284

Check warning on line 189 in SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Loop should be simplified by calling Select(alternative => alternative.Elements))
{
if (string.Equals(general.Name, "OperatorExpression", StringComparison.Ordinal))
if (alternative.Elements[0] is not TerminalElement { Value: "{" })
{
continue;
}

if (alternative.Elements[2] is not TerminalElement { Value: "}" })
{
return true;
continue;
}

if (alternative.Elements[1] is not NonTerminalElement nonTerminal)
{
continue;
}

if (!string.IsNullOrEmpty(nonTerminal.Suffix))
{
continue;
}

if (nonTerminal.Container is AssignmentElement)
{
continue;
}

return true;
}

return false;
Expand Down
30 changes: 26 additions & 4 deletions SysML2.NET.CodeGenerator/HandleBarHelpers/TerminalWriter.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="TerminalWriter.cs" company="Starion Group S.A.">
//
// Copyright 2022-2026 Starion Group S.A.
// Copyright (C) 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
// 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
//
Expand Down Expand Up @@ -52,12 +52,23 @@ internal static void WriteTerminalAppend(EncodedTextWriter writer, string termin
{
if (NewLineTerminals.Contains(terminalValue))
{
if (terminalValue == "}")
{
writer.WriteSafeString($"stringBuilder.DecreaseIndent();{Environment.NewLine}");
}

if (terminalValue == "{")
{
writer.WriteSafeString($"stringBuilder.Append(' ');{Environment.NewLine}");
}

writer.WriteSafeString($"stringBuilder.AppendLine(\"{terminalValue}\");");

if (terminalValue == "{")
{
writer.WriteSafeString($"{Environment.NewLine}stringBuilder.IncreaseIndent();");
}

return;
}

Expand Down Expand Up @@ -86,7 +97,18 @@ internal static void WriteTerminalAppendWithLeadingSpace(EncodedTextWriter write
{
if (NewLineTerminals.Contains(terminalValue))
{
if (terminalValue == "}")
{
writer.WriteSafeString($"stringBuilder.DecreaseIndent();{Environment.NewLine}");
}

writer.WriteSafeString($"stringBuilder.AppendLine(\"{terminalValue}\");");

if (terminalValue == "{")
{
writer.WriteSafeString($"{Environment.NewLine}stringBuilder.IncreaseIndent();");
}

return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="TextualNotationBuilderFacade.cs" company="Starion Group S.A.">
//
// Copyright 2022-2026 Starion Group S.A.
// Copyright (C) 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
// 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
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="{{ this.Context.Name }}TextualNotationBuilder.cs" company="Starion Group S.A.">
//
// Copyright 2022-2026 Starion Group S.A.
// Copyright (C) 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
// 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
//
Expand All @@ -25,7 +25,6 @@
namespace SysML2.NET.Serializer.TextualNotation.Writers
{
using System.Linq;
using System.Text;

using SysML2.NET.Core.POCO.Root.Elements;

Expand All @@ -41,8 +40,8 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers
/// </summary>
/// <param name="poco">The <see cref="{{ #NamedElement.WriteFullyQualifiedTypeName ../this.Context }}" /> from which the rule should be build</param>
/// <param name="writerContext">The <see cref="TextualNotationWriterContext" /> providing the serialization context for the current <paramref name="poco"/></param>
/// <param name="stringBuilder">The <see cref="StringBuilder" /> that contains the entire textual notation</param>
public static void Build{{rule.RuleName}}({{ #NamedElement.WriteFullyQualifiedTypeName ../this.Context }} poco, TextualNotationWriterContext writerContext, StringBuilder stringBuilder)
/// <param name="stringBuilder">The <see cref="IndentedStringBuilder" /> that accumulates the entire textual notation with indentation</param>
public static void Build{{rule.RuleName}}({{ #NamedElement.WriteFullyQualifiedTypeName ../this.Context }} poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
{{RulesHelper.WriteRule rule ../this.Context ../this.AllRules}}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="SharedTextualNotationBuilder.cs" company="Starion Group S.A.">
//
// Copyright 2022-2026 Starion Group S.A.
// Copyright (C) 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
// 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
//
Expand All @@ -25,7 +25,6 @@
namespace SysML2.NET.Serializer.TextualNotation.Writers
{
using System.Linq;
using System.Text;

using SysML2.NET.Core.POCO.Root.Elements;

Expand All @@ -44,8 +43,8 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers
/// </summary>
/// <param name="poco">The <see cref="{{ #NamedElement.WriteFullyQualifiedTypeName entry.TargetClass }}" /> from which the rule should be build</param>
/// <param name="writerContext">The <see cref="TextualNotationWriterContext" /> providing the serialization context for the current <paramref name="poco"/></param>
/// <param name="stringBuilder">The <see cref="StringBuilder" /> that contains the entire textual notation</param>
public static void Build{{entry.Rule.RuleName}}({{ #NamedElement.WriteFullyQualifiedTypeName entry.TargetClass }} poco, TextualNotationWriterContext writerContext, StringBuilder stringBuilder)
/// <param name="stringBuilder">The <see cref="IndentedStringBuilder" /> that accumulates the entire textual notation with indentation</param>
public static void Build{{entry.Rule.RuleName}}({{ #NamedElement.WriteFullyQualifiedTypeName entry.TargetClass }} poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
{{RulesHelper.WriteRule entry.Rule entry.TargetClass ../this.AllRules}}
}
Expand Down
Loading
Loading