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
65 changes: 65 additions & 0 deletions uml4net.CodeGenerator.Tests/CSharpKeywordsTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="CSharpKeywordsTestFixture.cs" company="Starion Group S.A.">
//
// Copyright (C) 2019-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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace uml4net.CodeGenerator.Tests
{
using System;
using System.Linq;

using Microsoft.CodeAnalysis.CSharp;

using NUnit.Framework;

using uml4net.Extensions;

/// <summary>
/// Verifies the list of reserved C# keywords of <see cref="StringExtensions"/> against the C# compiler. The
/// uml4net.Extensions library does not reference the compiler, this test project does.
/// </summary>
[TestFixture]
public class CSharpKeywordsTestFixture
{
[Test]
public void Verify_that_the_reserved_keywords_are_those_of_the_CSharp_compiler()
{
// the compiler also reserves four undocumented keywords that start with two underscores (__arglist,
// __makeref, __reftype and __refvalue); a model element is not expected to have such a name
var compilerKeywords = SyntaxFacts.GetReservedKeywordKinds()
.Select(SyntaxFacts.GetText)
.Where(x => !x.StartsWith("__", StringComparison.Ordinal))
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();

Assert.That(StringExtensions.QueryCSharpKeywords(), Is.EqualTo(compilerKeywords));
}

[Test]
public void Verify_that_a_contextual_keyword_is_not_a_reserved_keyword()
{
var contextualKeywords = SyntaxFacts.GetContextualKeywordKinds().Select(SyntaxFacts.GetText).ToList();

using (Assert.EnterMultipleScope())
{
Assert.That(contextualKeywords, Does.Contain("partial"));
Assert.That(contextualKeywords.Where(x => x.QueryIsCSharpKeyword()), Is.Empty);
}
}
}
}
62 changes: 62 additions & 0 deletions uml4net.Extensions.Tests/NamedElementExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

namespace uml4net.Extensions.Tests
{
using System;
using System.IO;
using System.Linq;
using Classification;
Expand Down Expand Up @@ -85,5 +86,66 @@ public void Verify_that_QueryNamespace_returns_expected_resul()
Assert.That(connector.QueryNamespace(), Is.EqualTo("UML.StructuredClassifiers.Connector"));
}
}

[Test]
public void Verify_that_QueryCSharpIdentifier_applies_the_casing_and_escapes_a_keyword()
{
var @class = new Property { Name = "class" };
var ownedComment = new Property { Name = "ownedComment" };
var upperCase = new Class { Name = "Event" };

using (Assert.EnterMultipleScope())
{
Assert.That(@class.QueryCSharpIdentifier(), Is.EqualTo("@class"), "as modelled, a keyword");
Assert.That(@class.QueryCSharpIdentifier(CSharpIdentifierCasing.LowerCaseFirstLetter), Is.EqualTo("@class"));
Assert.That(@class.QueryCSharpIdentifier(CSharpIdentifierCasing.UpperCaseFirstLetter), Is.EqualTo("Class"), "not a keyword after the casing is applied");

Assert.That(ownedComment.QueryCSharpIdentifier(), Is.EqualTo("ownedComment"));
Assert.That(ownedComment.QueryCSharpIdentifier(CSharpIdentifierCasing.UpperCaseFirstLetter), Is.EqualTo("OwnedComment"));

Assert.That(upperCase.QueryCSharpIdentifier(), Is.EqualTo("Event"));
Assert.That(upperCase.QueryCSharpIdentifier(CSharpIdentifierCasing.LowerCaseFirstLetter), Is.EqualTo("@event"), "a keyword after the casing is applied");
}
}

[Test]
public void Verify_that_QueryCSharpIdentifier_throws_for_an_element_without_a_name()
{
using (Assert.EnterMultipleScope())
{
Assert.That(() => new Property { XmiId = "id-1" }.QueryCSharpIdentifier(),
Throws.InvalidOperationException.With.Message.Contains("id-1").And.Message.Contains("Property"));
Assert.That(() => new Property { Name = " " }.QueryCSharpIdentifier(), Throws.InvalidOperationException);
Assert.That(() => NamedElementExtensions.QueryCSharpIdentifier(null), Throws.ArgumentNullException);
Assert.That(() => new Property().QueryCSharpIdentifier(), Throws.InvalidOperationException.With.Message.Contains("xmi:id ''"), "an element without a name and without an xmi:id");
Assert.That(() => new Property { Name = "class" }.QueryCSharpIdentifier((CSharpIdentifierCasing)99), Throws.TypeOf<ArgumentOutOfRangeException>());
}
}

[Test]
[NonParallelizable]
public void Verify_that_QueryCSharpIdentifier_escapes_a_registered_keyword()
{
var value = new Property { Name = "value" };
var upperCase = new Property { Name = "Var" };

try
{
StringExtensions.RegisterAdditionalCSharpKeywords("var", "value");

using (Assert.EnterMultipleScope())
{
Assert.That(value.QueryCSharpIdentifier(), Is.EqualTo("@value"));
Assert.That(value.QueryCSharpIdentifier(CSharpIdentifierCasing.UpperCaseFirstLetter), Is.EqualTo("Value"));
Assert.That(upperCase.QueryCSharpIdentifier(CSharpIdentifierCasing.LowerCaseFirstLetter), Is.EqualTo("@var"));
}
}
finally
{
StringExtensions.ClearAdditionalCSharpKeywords();
}

Assert.That(value.QueryCSharpIdentifier(), Is.EqualTo("value"));
}
}
}
163 changes: 163 additions & 0 deletions uml4net.Extensions.Tests/StringExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
namespace uml4net.Extensions.Tests
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using NUnit.Framework;

Expand Down Expand Up @@ -87,5 +89,166 @@ public void Verify_that_LowerCaseFirstLetter_throws_when_input_is_null_or_empty(
Assert.That(() => string.Empty.LowerCaseFirstLetter(), Throws.TypeOf<ArgumentException>());
}
}

[Test]
public void Verify_that_QueryIsCSharpKeyword_recognizes_the_reserved_keywords()
{
using (Assert.EnterMultipleScope())
{
Assert.That("class".QueryIsCSharpKeyword(), Is.True);
Assert.That("in".QueryIsCSharpKeyword(), Is.True);
Assert.That("true".QueryIsCSharpKeyword(), Is.True);
Assert.That("Class".QueryIsCSharpKeyword(), Is.False, "the comparison is case-sensitive");
Assert.That("var".QueryIsCSharpKeyword(), Is.False, "a contextual keyword is a legal identifier");
Assert.That("type".QueryIsCSharpKeyword(), Is.False);
Assert.That(string.Empty.QueryIsCSharpKeyword(), Is.False);
Assert.That(() => StringExtensions.QueryIsCSharpKeyword(null), Throws.ArgumentNullException);

Assert.That(StringExtensions.QueryCSharpKeywords(), Has.Count.EqualTo(77));
Assert.That(StringExtensions.QueryCSharpKeywords(), Is.Ordered.Using((IComparer<string>)StringComparer.Ordinal));
Assert.That(StringExtensions.QueryCSharpKeywords().All(x => x.QueryIsCSharpKeyword()), Is.True);
}
}

[Test]
public void Verify_that_EscapeCSharpKeyword_prefixes_a_reserved_keyword()
{
using (Assert.EnterMultipleScope())
{
Assert.That("class".EscapeCSharpKeyword(), Is.EqualTo("@class"));
Assert.That("event".EscapeCSharpKeyword(), Is.EqualTo("@event"));
Assert.That("Class".EscapeCSharpKeyword(), Is.EqualTo("Class"));
Assert.That("ownedComment".EscapeCSharpKeyword(), Is.EqualTo("ownedComment"));
Assert.That("var".EscapeCSharpKeyword(), Is.EqualTo("var"));
Assert.That("@class".EscapeCSharpKeyword(), Is.EqualTo("@class"), "a string that is already escaped is not escaped again");
Assert.That("class ".EscapeCSharpKeyword(), Is.EqualTo("class "), "the string is not trimmed");
Assert.That(string.Empty.EscapeCSharpKeyword(), Is.Empty);
Assert.That(() => StringExtensions.EscapeCSharpKeyword(null), Throws.ArgumentNullException);
}
}

[Test]
[NonParallelizable]
public void Verify_that_RegisterAdditionalCSharpKeywords_extends_the_reserved_keywords()
{
try
{
StringExtensions.RegisterAdditionalCSharpKeywords("var", "value");
StringExtensions.RegisterAdditionalCSharpKeywords("value", "class", "record");

using (Assert.EnterMultipleScope())
{
Assert.That("var".QueryIsCSharpKeyword(), Is.True);
Assert.That("value".QueryIsCSharpKeyword(), Is.True);
Assert.That("Var".QueryIsCSharpKeyword(), Is.False, "the comparison is case-sensitive");
Assert.That("class".QueryIsCSharpKeyword(), Is.True);

Assert.That("var".EscapeCSharpKeyword(), Is.EqualTo("@var"));
Assert.That("value".EscapeCSharpKeyword(), Is.EqualTo("@value"));
Assert.That("Value".EscapeCSharpKeyword(), Is.EqualTo("Value"));

Assert.That(StringExtensions.QueryAdditionalCSharpKeywords(), Is.EqualTo(new[] { "record", "value", "var" }),
"a reserved keyword of C# and a word that is registered twice are ignored");
Assert.That(StringExtensions.QueryCSharpKeywords(), Has.Count.EqualTo(77));
}
}
finally
{
StringExtensions.ClearAdditionalCSharpKeywords();
}

using (Assert.EnterMultipleScope())
{
Assert.That(StringExtensions.QueryAdditionalCSharpKeywords(), Is.Empty);
Assert.That("var".QueryIsCSharpKeyword(), Is.False);
Assert.That("value".EscapeCSharpKeyword(), Is.EqualTo("value"));
}
}

[Test]
[NonParallelizable]
public void Verify_that_RegisterAdditionalCSharpKeywords_rejects_invalid_keywords()
{
try
{
using (Assert.EnterMultipleScope())
{
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords(null), Throws.ArgumentNullException);
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords("var", null), Throws.ArgumentException);
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords("var", string.Empty), Throws.ArgumentException);
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords("var", " "), Throws.ArgumentException);
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords("var", " value"), Throws.ArgumentException, "a word that starts with white space never equals a name");
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords("var", "value "), Throws.ArgumentException);
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords("var", "@value"), Throws.ArgumentException);

Assert.That(StringExtensions.QueryAdditionalCSharpKeywords(), Is.Empty, "nothing is registered when one of the keywords is invalid");
Assert.That(() => StringExtensions.RegisterAdditionalCSharpKeywords(), Throws.Nothing);
}
}
finally
{
StringExtensions.ClearAdditionalCSharpKeywords();
}
}

[Test]
[NonParallelizable]
public void Verify_that_the_keyword_queries_return_a_read_only_snapshot()
{
try
{
StringExtensions.RegisterAdditionalCSharpKeywords("var");

var snapshot = StringExtensions.QueryAdditionalCSharpKeywords();

StringExtensions.RegisterAdditionalCSharpKeywords("value");
StringExtensions.ClearAdditionalCSharpKeywords();

using (Assert.EnterMultipleScope())
{
Assert.That(snapshot, Is.EqualTo(new[] { "var" }), "a later registration or clear does not change a list that was returned before");
Assert.That(() => ((IList<string>)snapshot).Add("x"), Throws.TypeOf<NotSupportedException>());
Assert.That(() => ((IList<string>)StringExtensions.QueryCSharpKeywords()).Add("x"), Throws.TypeOf<NotSupportedException>());
Assert.That(StringExtensions.QueryCSharpKeywords(), Has.Count.EqualTo(77));
}
}
finally
{
StringExtensions.ClearAdditionalCSharpKeywords();
}
}

[Test]
[NonParallelizable]
public void Verify_that_the_additional_keywords_can_be_registered_and_queried_from_several_threads()
{
const int threads = 8;
const int wordsPerThread = 200;

try
{
var tasks = Enumerable.Range(0, threads).Select(thread => Task.Run(() =>
{
for (var i = 0; i < wordsPerThread; i++)
{
var word = $"word_{thread}_{i}";

StringExtensions.RegisterAdditionalCSharpKeywords(word);

if (!word.QueryIsCSharpKeyword() || !"class".QueryIsCSharpKeyword() || "Class".QueryIsCSharpKeyword())
{
throw new InvalidOperationException($"{word} is not found after it has been registered");
}
}
})).ToArray();

Assert.That(() => Task.WaitAll(tasks), Throws.Nothing);
Assert.That(StringExtensions.QueryAdditionalCSharpKeywords(), Has.Count.EqualTo(threads * wordsPerThread), "no registration is lost");
}
finally
{
StringExtensions.ClearAdditionalCSharpKeywords();
}
}
}
}
44 changes: 44 additions & 0 deletions uml4net.Extensions/CSharpIdentifierCasing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="CSharpIdentifierCasing.cs" company="Starion Group S.A.">
//
// Copyright (C) 2019-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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace uml4net.Extensions
{
/// <summary>
/// The casing of the first letter of a C# identifier that is derived from the name of a model element, see
/// <see cref="NamedElementExtensions.QueryCSharpIdentifier"/>
/// </summary>
public enum CSharpIdentifierCasing
{
/// <summary>
/// The name is used as it is in the model
/// </summary>
AsModelled = 0,

/// <summary>
/// The first letter of the name is converted to upper case
/// </summary>
UpperCaseFirstLetter = 1,

/// <summary>
/// The first letter of the name is converted to lower case
/// </summary>
LowerCaseFirstLetter = 2
}
}
Loading
Loading