Skip to content

[Add] C# identifier naming with escaping of reserved keywords; fixes #513 - #519

Merged
samatstariongroup merged 3 commits into
developmentfrom
GH513-csharp-identifiers
Oct 13, 2026
Merged

samatstariongroup merged 3 commits into
developmentfrom
GH513-csharp-identifiers

Conversation

@samatstariongroup

@samatstariongroup samatstariongroup commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Fixes #513

Changes

New in uml4net.Extensions:

  • string.QueryIsCSharpKeyword(): whether a string is one of the 77 reserved keywords of C#, or a word that a project has registered. Case-sensitive. A contextual keyword such as var or value is a legal identifier and is not a reserved keyword, unless it has been registered.
  • string.EscapeCSharpKeyword(): the string prefixed with @ when it is a reserved keyword, or a registered word, unchanged otherwise.
  • StringExtensions.QueryCSharpKeywords(): the list of reserved keywords, in alphabetical order. Registered words are not included.
  • StringExtensions.RegisterAdditionalCSharpKeywords(params string[] keywords): registers words that are escaped like a reserved keyword, for example var and value. The registration is static and applies to the whole process, so it also reaches QueryCSharpIdentifier and the Handlebars helpers. A generator calls it once, before it generates. A null, empty or white-space word throws an ArgumentException and nothing of that call is registered; the same holds for a word that starts or ends with white space or starts with @, because such a word never equals a name. A word that is a reserved keyword or is already registered is ignored.
  • StringExtensions.QueryAdditionalCSharpKeywords(): the registered words, in alphabetical order.
  • StringExtensions.ClearAdditionalCSharpKeywords(): removes the registered words.
  • INamedElement.QueryCSharpIdentifier(CSharpIdentifierCasing casing = AsModelled): the name of the element with the requested casing of its first letter, escaped when the result is a keyword. Throws an InvalidOperationException that names the xmi:id for an element without a name. Throws an ArgumentOutOfRangeException for a casing that is not a value of the enumeration.
  • The enumeration CSharpIdentifierCasing: AsModelled, UpperCaseFirstLetter, LowerCaseFirstLetter.

New Handlebars helpers in uml4net.HandleBars:

  • String.EscapeCSharpKeyword, with a string as argument
  • NamedElement.WriteCSharpIdentifier, with the element and optionally the casing ("AsModelled", "UpperCaseFirstLetter" or "LowerCaseFirstLetter", not case-sensitive) as arguments Only these three names are accepted: a number, a list of names or a value that is not a string throws an ArgumentException.

uml4net itself now uses the new methods:

  • Property.WriteForClass escaped the name of the backing field of a containment property for six names: class, interface, namespace, object, type and using. It now calls EscapeCSharpKeyword, so all reserved keywords are escaped. type is not a keyword and is no longer escaped.
  • Operation.WriteForPOCOInterface, Operation.WriteForPOCOExtend and Operation.WriteForPOCOClass escape the name of a parameter. A parameter named in, ref or event gave code that does not compile.

The generated code of uml4net does not change: the only containment property of the UML metamodel that is named as a keyword is object, which was escaped before and still is, and no operation parameter has such a name. The tests of uml4net.CodeGenerator.Tests that compare with the expected files pass.

The keyword list

The list of reserved keywords is fixed in uml4net.Extensions, which targets .NET Standard 2.0 and does not reference Roslyn. CSharpKeywordsTestFixture in uml4net.CodeGenerator.Tests, a project that does reference Roslyn, compares it with SyntaxFacts.GetReservedKeywordKinds(), so a difference with the compiler fails a test. The compiler also reserves four undocumented keywords that start with two underscores (__arglist, __makeref, __reftype, __refvalue); these are left out.

A project extends the list with RegisterAdditionalCSharpKeywords. The registered words are kept apart from the fixed list, so the comparison with the compiler is not affected. The registration is done from C#; there is no Handlebars helper for it.

What the downstream projects have today

Project Today With uml4net
SysML2.NET ReservedCSharpNameMapper escapes in, out, ref and var, and maps operator symbols and true/false to names all reserved keywords are escaped; var is escaped when SysML2.NET registers it with RegisterAdditionalCSharpKeywords. The mapping of symbols stays in SysML2.NET; true and false become @true and @false unless it keeps its mapping.
mycelium-sdk ReservedCSharpNameMapper asks Roslyn the same set of keywords, except the four that start with two underscores, which Roslyn also reports
EORSADB the helper At writes @ in front of its argument String.EscapeCSharpKeyword only writes @ for a keyword
Property.WritePropertyName (SysML2.NET, mycelium-forge, mycelium-sdk) and EnumerationLiteral.Write each project its own casing NamedElement.WriteCSharpIdentifier with the casing as argument. The rule that a derived property starts with a lower-case letter stays in SysML2.NET and mycelium-forge.

Tests

  • StringExtensionsTestFixture and NamedElementExtensionsTestFixture: nine tests for the new methods, including an element without a name, a casing outside the enumeration, the null argument, the registration of additional words with its invalid input, the read-only snapshots, and registration from eight threads at once.
  • StringHelperTestFixture and NamedElementHelperTestFixture: five tests for the two new helpers, including a registered word and a casing argument that is not one of the three names.
  • OperationHelperTestFixture: a parameter that is named as a keyword is escaped by the three operation helpers; with several parameters only the keywords and the registered words are escaped.
  • PropertyHelperTestFixture: Property.WriteForClass escapes the backing field for event and class, not for type, and for value only when it is registered.
  • CSharpKeywordsTestFixture: the list against the compiler, and no contextual keyword in the list.
  • Full suite: all 1,209 tests pass. All 117 lines that this pull request adds to uml4net.Extensions and uml4net.HandleBars are covered.

Not in this pull request

  • The entries for the two helpers on the wiki page UML Handlebars Helpers, to be added when this is released.
  • DocumentationHelper writes the name of a parameter in a <param name="..."> tag without @, which is what the compiler expects there.

samatstarion added 3 commits October 11, 2026 18:16
…asing of the first letter chosen by the caller; fixes #513
…ds, such as var and value, that are escaped like a reserved keyword; refs #513
…accepts the three names, an additional keyword that can never equal a name is rejected; tests for the review findings; refs #513
@sonarqubecloud

Copy link
Copy Markdown

@samatstariongroup
samatstariongroup merged commit 377d6ad into development Oct 13, 2026
8 checks passed
@samatstariongroup
samatstariongroup deleted the GH513-csharp-identifiers branch October 13, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Provide C# identifier naming (reserved keywords, unnamed elements) in uml4net instead of in each downstream generator

1 participant