From 6eca6e10de998b07817f72a7a3818f1dd5c0dc90 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Fri, 5 Dec 2025 09:10:38 +0100 Subject: [PATCH 1/6] Add support for nullable reference types --- Diacritics/Diacritics.csproj | 6 +- Diacritics/DiacriticsMapper.cs | 77 ++++++------ Diacritics/Extensions/StringExtensions.cs | 17 ++- Diacritics/IDiacriticsMapper.cs | 12 +- Diacritics/Internals/Nullable.cs | 64 ++++++++++ Diacritics/Internals/StringBuilderCache.cs | 2 +- Diacritics/Model/MappingReplacement.cs | 10 +- ReleaseNotes.txt | 3 + .../Diacritics.Benchmark.csproj | 3 +- .../RemoveDiacriticsBenchmark.cs | 8 +- .../Diacritics.Tests/Diacritics.Tests.csproj | 6 +- .../Diacritics.Tests/DiacriticsMapperTests.cs | 112 +++++++++--------- .../Extensions/StringExtensionsTests.cs | 4 +- .../Import/Model/AccentsMapping.cs | 9 +- .../Import/Model/AccentsMappingData.cs | 12 +- .../Model/AccentsMappingDataJsonConverter.cs | 16 +-- .../Diacritics.Tests/Import/Model/Metadata.cs | 14 ++- azure-pipelines.yml | 4 +- 18 files changed, 237 insertions(+), 142 deletions(-) create mode 100644 Diacritics/Internals/Nullable.cs diff --git a/Diacritics/Diacritics.csproj b/Diacritics/Diacritics.csproj index 2df07d6..e7584bc 100644 --- a/Diacritics/Diacritics.csproj +++ b/Diacritics/Diacritics.csproj @@ -1,9 +1,11 @@  - net462;netstandard1.2;netstandard2.0;netstandard2.1;net7.0;net8.0;net9.0 + net48;netstandard1.2;netstandard2.0;netstandard2.1;net7.0;net8.0;net9.0 Library True + latest + enable @@ -28,7 +30,6 @@ snupkg true true - True @@ -40,7 +41,6 @@ true - diff --git a/Diacritics/DiacriticsMapper.cs b/Diacritics/DiacriticsMapper.cs index 992357c..358760b 100644 --- a/Diacritics/DiacriticsMapper.cs +++ b/Diacritics/DiacriticsMapper.cs @@ -1,19 +1,20 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Diagnostics.CodeAnalysis; using System.Threading; using Diacritics.AccentMappings; using Diacritics.Internals; +// ReSharper disable ConvertIfStatementToNullCoalescingAssignment + namespace Diacritics { public class DiacriticsMapper : IDiacriticsMapper { #region DiacriticsMapper.Current - private static Lazy Implementation; + private static Lazy Implementation = null!; static DiacriticsMapper() { @@ -43,35 +44,37 @@ public DiacriticsMapper(params IAccentMapping[] mappings) private static IDictionary ConvertMappings(IAccentMapping[] accentMappings) { + if (accentMappings == null) + { + throw new ArgumentNullException(nameof(accentMappings)); + } + var all = new Dictionary(); - if (accentMappings != null) + foreach (var accentMapping in accentMappings) { - foreach (var accentMapping in accentMappings) + var mappings = accentMapping.Mapping; + foreach (var mapping in mappings) { - var mappings = accentMapping.Mapping; - foreach (var mapping in mappings) + if (!all.TryGetValue(mapping.Key, out var mappingReplacement)) { - if (!all.TryGetValue(mapping.Key, out var mappingReplacement)) + all[mapping.Key] = mapping.Value; + } + else + { + // Merge existing DecomposeTitle and Decompose properties, + // unless the current mapping replacement defines them. + if (mappingReplacement.DecomposeTitle == null) { - all[mapping.Key] = mapping.Value; + mappingReplacement.DecomposeTitle = mapping.Value.DecomposeTitle; } - else + + if (mappingReplacement.Decompose == null) { - // Merge existing DecomposeTitle and Decompose properties, - // unless the current mapping replacement defines them. - if (mappingReplacement.DecomposeTitle == null) - { - mappingReplacement.DecomposeTitle = mapping.Value.DecomposeTitle; - } - - if (mappingReplacement.Decompose == null) - { - mappingReplacement.Decompose = mapping.Value.Decompose; - } - - all[mapping.Key] = mappingReplacement; + mappingReplacement.Decompose = mapping.Value.Decompose; } + + all[mapping.Key] = mappingReplacement; } } } @@ -90,28 +93,33 @@ IEnumerator IEnumerable.GetEnumerator() return this.GetEnumerator(); } - public string RemoveDiacritics(string source) + [return: NotNullIfNotNull(nameof(source))] + public string? RemoveDiacritics(string? source) { return this.RemoveDiacritics(source, options: null); } - public string RemoveDiacritics(string source, DiacriticsOptions options) + [return: NotNullIfNotNull(nameof(source))] + public string? RemoveDiacritics(string? source, DiacriticsOptions? options) { return RemoveDiacritics(source, this.diacriticsMappings, options); } - public string RemoveDiacritics(string source, IAccentMapping[] mappings) + [return: NotNullIfNotNull(nameof(source))] + public string? RemoveDiacritics(string? source, IAccentMapping[] mappings) { return this.RemoveDiacritics(source, mappings, options: null); } - public string RemoveDiacritics(string source, IAccentMapping[] mappings, DiacriticsOptions options) + [return: NotNullIfNotNull(nameof(source))] + public string? RemoveDiacritics(string? source, IAccentMapping[] mappings, DiacriticsOptions? options) { var diacriticsMappings = ConvertMappings(mappings); return RemoveDiacritics(source, diacriticsMappings, options); } - private static string RemoveDiacritics(string source, IDictionary diacriticsMappings, DiacriticsOptions options) + [return: NotNullIfNotNull(nameof(source))] + private static string? RemoveDiacritics(string? source, IDictionary diacriticsMappings, DiacriticsOptions? options) { if (string.IsNullOrWhiteSpace(source)) { @@ -120,7 +128,8 @@ private static string RemoveDiacritics(string source, IDictionary - public static string RemoveDiacritics(this string source) + [return: NotNullIfNotNull(nameof(source))] + public static string? RemoveDiacritics(this string? source) { return DiacriticsMapper.Current.RemoveDiacritics(source); } /// - public static string RemoveDiacritics(this string source, DiacriticsOptions options) + [return: NotNullIfNotNull(nameof(source))] + public static string? RemoveDiacritics(this string? source, DiacriticsOptions options) { return DiacriticsMapper.Current.RemoveDiacritics(source, options); } /// - public static string RemoveDiacritics(this string source, params IAccentMapping[] mappings) + [return: NotNullIfNotNull(nameof(source))] + public static string? RemoveDiacritics(this string? source, params IAccentMapping[] mappings) { return DiacriticsMapper.Current.RemoveDiacritics(source, mappings); } /// - public static string RemoveDiacritics(this string source, IAccentMapping[] mappings, DiacriticsOptions options) + [return: NotNullIfNotNull(nameof(source))] + public static string? RemoveDiacritics(this string? source, IAccentMapping[] mappings, DiacriticsOptions options) { return DiacriticsMapper.Current.RemoveDiacritics(source, mappings, options); } /// - public static bool HasDiacritics(this string source) + public static bool HasDiacritics(this string? source) { return DiacriticsMapper.Current.HasDiacritics(source); } /// - public static bool HasDiacritics(this string source, DiacriticsOptions options) + public static bool HasDiacritics(this string? source, DiacriticsOptions options) { return DiacriticsMapper.Current.HasDiacritics(source, options); } diff --git a/Diacritics/IDiacriticsMapper.cs b/Diacritics/IDiacriticsMapper.cs index 8502720..9c5ad89 100644 --- a/Diacritics/IDiacriticsMapper.cs +++ b/Diacritics/IDiacriticsMapper.cs @@ -16,21 +16,21 @@ public interface IDiacriticsMapper : IEnumerable. /// /// The source string. - string RemoveDiacritics(string source); + string? RemoveDiacritics(string? source); /// /// Removes any diacritical characters from . /// /// The source string. /// The options. - string RemoveDiacritics(string source, DiacriticsOptions options); + string? RemoveDiacritics(string? source, DiacriticsOptions? options); /// /// Removes any diacritical characters from using custom accent mappings. /// /// The source string. /// The accent mapping to be used to replace diacritical characters. - string RemoveDiacritics(string source, IAccentMapping[] mappings); + string? RemoveDiacritics(string? source, IAccentMapping[] mappings); /// /// Removes any diacritical characters from using custom accent mappings. @@ -38,14 +38,14 @@ public interface IDiacriticsMapper : IEnumerableThe source string. /// The accent mapping to be used to replace diacritical characters. /// The options. - string RemoveDiacritics(string source, IAccentMapping[] mappings, DiacriticsOptions options); + string? RemoveDiacritics(string? source, IAccentMapping[] mappings, DiacriticsOptions? options); /// /// Checks if the given string contains any diacritical characters. /// /// The source string. /// true if the source string contains any diacritical characters; otherwise, false. - bool HasDiacritics(string source); + bool HasDiacritics(string? source); /// /// Checks if the given string contains any diacritical characters. @@ -53,6 +53,6 @@ public interface IDiacriticsMapper : IEnumerableThe source string. /// The options. /// true if the source string contains any diacritical characters; otherwise, false. - bool HasDiacritics(string source, DiacriticsOptions options); + bool HasDiacritics(string? source, DiacriticsOptions? options); } } \ No newline at end of file diff --git a/Diacritics/Internals/Nullable.cs b/Diacritics/Internals/Nullable.cs new file mode 100644 index 0000000..8e27d33 --- /dev/null +++ b/Diacritics/Internals/Nullable.cs @@ -0,0 +1,64 @@ +#if NET48_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + /* + * Nullable Reference Types (NRT) are a compiler feature introduced with C# 8. + * Older target frameworks (.NET Framework 4.8, .NET Standard 2.0 and earlier) + * do not provide the nullable-analysis attributes that newer frameworks include. + * + * These attribute stubs are included only for those older TFMs so the compiler + * can emit correct nullability metadata and consumers can benefit from NRT + * annotations. They have no runtime behavior and should NOT be included when the + * framework already provides them (.NET Core 3.0+, .NET Standard 2.1+, .NET 5+). + */ + + [ExcludeFromCodeCoverage] + [DebuggerNonUserCode] + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public MaybeNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + } + + [ExcludeFromCodeCoverage] + [DebuggerNonUserCode] + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public NotNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + } + + [ExcludeFromCodeCoverage] + [DebuggerNonUserCode] + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + AllowMultiple = true, + Inherited = false + )] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + public string ParameterName { get; } + + public NotNullIfNotNullAttribute(string parameterName) + { + this.ParameterName = parameterName; + } + } + +#if NETSTANDARD1_2 + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + internal sealed class ExcludeFromCodeCoverage : Attribute; +#endif +} +#endif \ No newline at end of file diff --git a/Diacritics/Internals/StringBuilderCache.cs b/Diacritics/Internals/StringBuilderCache.cs index 8b867ee..012a132 100644 --- a/Diacritics/Internals/StringBuilderCache.cs +++ b/Diacritics/Internals/StringBuilderCache.cs @@ -17,7 +17,7 @@ internal static class StringBuilderCache private const int DefaultCapacity = 16; [ThreadStatic] - private static StringBuilder CachedInstance; + private static StringBuilder? CachedInstance; /// /// Acquires a cached instance of if one exists otherwise a new instance. diff --git a/Diacritics/Model/MappingReplacement.cs b/Diacritics/Model/MappingReplacement.cs index 89035a4..5192f82 100644 --- a/Diacritics/Model/MappingReplacement.cs +++ b/Diacritics/Model/MappingReplacement.cs @@ -4,18 +4,18 @@ namespace Diacritics { public struct MappingReplacement : IEquatable { - public MappingReplacement(string @base, string decompose, string decomposeTitle) : this() + public MappingReplacement(string? @base, string? decompose, string? decomposeTitle) : this() { this.Base = @base; this.Decompose = decompose; this.DecomposeTitle = decomposeTitle; } - public string Base { get; set; } + public string? Base { get; set; } - public string Decompose { get; set; } + public string? Decompose { get; set; } - public string DecomposeTitle { get; set; } + public string? DecomposeTitle { get; set; } public static implicit operator MappingReplacement(string value) => new MappingReplacement(value, null, null); @@ -26,7 +26,7 @@ public bool Equals(MappingReplacement other) string.Equals(this.DecomposeTitle, other.DecomposeTitle, StringComparison.Ordinal); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MappingReplacement other && this.Equals(other); } diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 090e9d3..c2c2cb0 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,3 +1,6 @@ +4.1 +- Support for nullable reference types. + 4.0 - Use cached StringBuilder instances to improve memory usage and reduce garbage collection pressure. - Remove unsupported target frameworks, update dependencies, maintenance and cleanup. diff --git a/Tests/Diacritics.Benchmark/Diacritics.Benchmark.csproj b/Tests/Diacritics.Benchmark/Diacritics.Benchmark.csproj index 077f3c6..8224d89 100644 --- a/Tests/Diacritics.Benchmark/Diacritics.Benchmark.csproj +++ b/Tests/Diacritics.Benchmark/Diacritics.Benchmark.csproj @@ -4,11 +4,12 @@ Exe net9.0 enable + latest enable - + diff --git a/Tests/Diacritics.Benchmark/RemoveDiacriticsBenchmark.cs b/Tests/Diacritics.Benchmark/RemoveDiacriticsBenchmark.cs index ed56ecb..3cc90fe 100644 --- a/Tests/Diacritics.Benchmark/RemoveDiacriticsBenchmark.cs +++ b/Tests/Diacritics.Benchmark/RemoveDiacriticsBenchmark.cs @@ -14,8 +14,8 @@ public class RemoveDiacriticsBenchmark private static readonly IResourceLoader ResourceLoader = System.Reflection.ResourceLoader.Current; private static readonly IDiacriticsMapper DiacriticsMapper = IDiacriticsMapper.Current; - private string loremIpsum100K; - private string loremIpsum1M; + private string? loremIpsum100K; + private string? loremIpsum1M; [GlobalSetup] public void Setup() @@ -25,13 +25,13 @@ public void Setup() } [Benchmark] - public string RemoveDiacritics_100kWords() + public string? RemoveDiacritics_100kWords() { return DiacriticsMapper.RemoveDiacritics(this.loremIpsum100K); } [Benchmark] - public string RemoveDiacritics_1mWords() + public string? RemoveDiacritics_1mWords() { return DiacriticsMapper.RemoveDiacritics(this.loremIpsum1M); } diff --git a/Tests/Diacritics.Tests/Diacritics.Tests.csproj b/Tests/Diacritics.Tests/Diacritics.Tests.csproj index 3a5e892..7cc131c 100644 --- a/Tests/Diacritics.Tests/Diacritics.Tests.csproj +++ b/Tests/Diacritics.Tests/Diacritics.Tests.csproj @@ -3,6 +3,8 @@ net9.0 false + latest + enable @@ -12,8 +14,8 @@ - - + + diff --git a/Tests/Diacritics.Tests/DiacriticsMapperTests.cs b/Tests/Diacritics.Tests/DiacriticsMapperTests.cs index 337c989..3b4eeb0 100644 --- a/Tests/Diacritics.Tests/DiacriticsMapperTests.cs +++ b/Tests/Diacritics.Tests/DiacriticsMapperTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using Diacritics.AccentMappings; +using Diacritics.AccentMappings; using FluentAssertions; using Xunit; @@ -15,14 +13,14 @@ public void ShouldNotRemoveDiacritics_IfNoMappingsAvailable() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(); - const string InputText = "ètôile"; - const string ExpectedText = "ètôile"; + const string inputText = "ètôile"; + const string expectedText = "ètôile"; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText); + var output = diacriticsMapper.RemoveDiacritics(inputText); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -30,14 +28,14 @@ public void ShouldRemoveDiacritics_WithSingleMapping() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "Delémont"; - const string ExpectedText = "Delemont"; + const string inputText = "Delémont"; + const string expectedText = "Delemont"; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText); + var output = diacriticsMapper.RemoveDiacritics(inputText); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -45,14 +43,14 @@ public void ShouldNotRemoveDiacritics_IfTheyAreNotPartOfTheMapping() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "ètöile"; - const string ExpectedText = "etöile"; + const string inputText = "ètöile"; + const string expectedText = "etöile"; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText); + var output = diacriticsMapper.RemoveDiacritics(inputText); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -60,14 +58,14 @@ public void ShouldRemoveDiacritics_WithMultipleMappings() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping(), new GermanAccentsMapping()); - const string InputText = "ètöile"; - const string ExpectedText = "etoile"; + const string inputText = "ètöile"; + const string expectedText = "etoile"; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText); + var output = diacriticsMapper.RemoveDiacritics(inputText); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -75,14 +73,14 @@ public void ShouldRemoveDiacritics_FromUppercaseCharacters() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "Ètoilé"; - const string ExpectedText = "Etoile"; + const string inputText = "Ètoilé"; + const string expectedText = "Etoile"; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText); + var output = diacriticsMapper.RemoveDiacritics(inputText); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -90,14 +88,14 @@ public void ShouldRemoveDiacritics_CombinedCedilleDiacritics() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "François"; - const string ExpectedText = "Francois"; + const string inputText = "François"; + const string expectedText = "Francois"; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText); + var output = diacriticsMapper.RemoveDiacritics(inputText); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -105,8 +103,8 @@ public void ShouldRemoveEszett() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new GermanAccentsMapping()); - const string InputText = "Paßstraße"; - const string ExpectedText = "Passstrasse"; + const string inputText = "Paßstraße"; + const string expectedText = "Passstrasse"; var options = new DiacriticsOptions { @@ -114,10 +112,10 @@ public void ShouldRemoveEszett() }; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText, options); + var output = diacriticsMapper.RemoveDiacritics(inputText, options); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -125,8 +123,8 @@ public void ShouldRemoveUmlaut_Decomposed() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FinnishAccentsMapping(), new GermanAccentsMapping()); - const string InputText = "Gefäß"; - const string ExpectedText = "Gefaess"; + const string inputText = "Gefäß"; + const string expectedText = "Gefaess"; var options = new DiacriticsOptions { @@ -134,10 +132,10 @@ public void ShouldRemoveUmlaut_Decomposed() }; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText, options); + var output = diacriticsMapper.RemoveDiacritics(inputText, options); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -145,8 +143,8 @@ public void ShouldRemoveFirstCharacter_WithSingleMapping_AndDecomposeFalse() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "épaule"; - const string ExpectedText = "epaule"; + const string inputText = "épaule"; + const string expectedText = "epaule"; var options = new DiacriticsOptions { @@ -154,10 +152,10 @@ public void ShouldRemoveFirstCharacter_WithSingleMapping_AndDecomposeFalse() }; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText, options); + var output = diacriticsMapper.RemoveDiacritics(inputText, options); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -165,8 +163,8 @@ public void ShouldRemoveFirstCharacter_WithSingleMapping_AndDecomposeTrue() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new GermanAccentsMapping()); - const string InputText = "Ärzte"; - const string ExpectedText = "Aerzte"; + const string inputText = "Ärzte"; + const string expectedText = "Aerzte"; var options = new DiacriticsOptions { @@ -174,10 +172,10 @@ public void ShouldRemoveFirstCharacter_WithSingleMapping_AndDecomposeTrue() }; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText, options); + var output = diacriticsMapper.RemoveDiacritics(inputText, options); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -185,8 +183,8 @@ public void ShouldRemoveFirstCharacter_WithMultipleMappings_AndDecomposeFalse() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping(), new GermanAccentsMapping()); - const string InputText = "épaule"; - const string ExpectedText = "epaule"; + const string inputText = "épaule"; + const string expectedText = "epaule"; var options = new DiacriticsOptions { @@ -194,10 +192,10 @@ public void ShouldRemoveFirstCharacter_WithMultipleMappings_AndDecomposeFalse() }; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText, options); + var output = diacriticsMapper.RemoveDiacritics(inputText, options); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } [Fact] @@ -205,8 +203,8 @@ public void ShouldRemoveFirstCharacter_WithMultipleMappings_AndDecomposeTrue() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping(), new GermanAccentsMapping()); - const string InputText = "épaule"; - const string ExpectedText = "epaule"; + const string inputText = "épaule"; + const string expectedText = "epaule"; var options = new DiacriticsOptions { @@ -214,10 +212,10 @@ public void ShouldRemoveFirstCharacter_WithMultipleMappings_AndDecomposeTrue() }; // Act - var output = diacriticsMapper.RemoveDiacritics(InputText, options); + var output = diacriticsMapper.RemoveDiacritics(inputText, options); // Assert - output.Should().Be(ExpectedText); + output.Should().Be(expectedText); } #endregion @@ -228,10 +226,10 @@ public void ShouldReturnFalseIfHasNoMappings() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(); - const string InputText = "ètôile"; + const string inputText = "ètôile"; // Act - var output = diacriticsMapper.HasDiacritics(InputText); + var output = diacriticsMapper.HasDiacritics(inputText); // Assert output.Should().BeFalse(); @@ -242,10 +240,10 @@ public void ShouldReturnFalseIfHasNoDiacritics() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "etoile"; + const string inputText = "etoile"; // Act - var output = diacriticsMapper.HasDiacritics(InputText); + var output = diacriticsMapper.HasDiacritics(inputText); // Assert output.Should().BeFalse(); @@ -256,10 +254,10 @@ public void ShouldReturnTrueIfHasDiacritics() { // Arrange IDiacriticsMapper diacriticsMapper = new DiacriticsMapper(new FrenchAccentsMapping()); - const string InputText = "ètôile"; + const string inputText = "ètôile"; // Act - var output = diacriticsMapper.HasDiacritics(InputText); + var output = diacriticsMapper.HasDiacritics(inputText); // Assert output.Should().BeTrue(); diff --git a/Tests/Diacritics.Tests/Extensions/StringExtensionsTests.cs b/Tests/Diacritics.Tests/Extensions/StringExtensionsTests.cs index c86b0b7..50aa54e 100644 --- a/Tests/Diacritics.Tests/Extensions/StringExtensionsTests.cs +++ b/Tests/Diacritics.Tests/Extensions/StringExtensionsTests.cs @@ -31,7 +31,7 @@ public void ShouldCallRemoveDiacriticsOnCustomMapperWhenCallRemoveDiacritics() [Theory] [ClassData(typeof(DiacriticsTestData))] - public void ShouldRemoveDiacritics(string input, (bool, string) expectedOutput) + public void ShouldRemoveDiacritics(string? input, (bool, string?) expectedOutput) { // Act var hasDiacritics = input.HasDiacritics(); @@ -42,7 +42,7 @@ public void ShouldRemoveDiacritics(string input, (bool, string) expectedOutput) hasDiacritics.Should().Be(expectedOutput.Item1); } - public class DiacriticsTestData : TheoryData + public class DiacriticsTestData : TheoryData { public DiacriticsTestData() { diff --git a/Tests/Diacritics.Tests/Import/Model/AccentsMapping.cs b/Tests/Diacritics.Tests/Import/Model/AccentsMapping.cs index 68347f0..0b2ae41 100644 --- a/Tests/Diacritics.Tests/Import/Model/AccentsMapping.cs +++ b/Tests/Diacritics.Tests/Import/Model/AccentsMapping.cs @@ -5,8 +5,15 @@ namespace Diacritics.Tests.Import { public class AccentsMapping { - public Metadata Metadata { get; set; } + public AccentsMapping() + { + this.Data = new List(); + } + [JsonProperty("metadata")] + public Metadata? Metadata { get; set; } + + [JsonProperty("data")] [JsonConverter(typeof(AccentsMappingDataJsonConverter))] public List Data { get; set; } } diff --git a/Tests/Diacritics.Tests/Import/Model/AccentsMappingData.cs b/Tests/Diacritics.Tests/Import/Model/AccentsMappingData.cs index 6b961a2..f7ba1a6 100644 --- a/Tests/Diacritics.Tests/Import/Model/AccentsMappingData.cs +++ b/Tests/Diacritics.Tests/Import/Model/AccentsMappingData.cs @@ -2,17 +2,17 @@ namespace Diacritics.Tests.Import { - [DebuggerDisplay("Mapping '{this.Source}' -> '{this.Target}' ({this.Case})")] + [DebuggerDisplay("Mapping '{this.Source}' -> '{this.Decompose}' ({this.Case})")] public class AccentsMappingData { - public char Source { get; set; } + public char Source { get; init; } - public string Base { get; set; } + public string? Base { get; init; } - public string Decompose { get; set; } + public string? Decompose { get; init; } - public string DecomposeTitle { get; set; } + public string? DecomposeTitle { get; init; } - public string Case { get; set; } + public string? Case { get; init; } } } diff --git a/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs b/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs index b0058a8..8dd63a8 100644 --- a/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs +++ b/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs @@ -7,10 +7,10 @@ namespace Diacritics.Tests.Import { public class AccentsMappingDataJsonConverter : JsonConverter { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var token = JToken.Load(reader); - var list = Activator.CreateInstance(objectType) as System.Collections.IList; + var list = (Activator.CreateInstance(objectType) as System.Collections.IList)!; //var itemType = objectType.GenericTypeArguments[0]; if (token.Type.ToString() == "Object") { @@ -26,7 +26,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var childName = nameProp[0]; var childValue = ((JProperty)child).Value; - var @case = childValue["case"].Value(); + var @case = childValue["case"]?.Value(); //if (@case == "upper") //{ // continue; @@ -34,20 +34,20 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var mapping = childValue["mapping"]; - string @base = null; + string? @base = null; if (mapping["base"] is JToken baseToken) { @base = baseToken.Value(); } - string decompose = null; - string decomposeTitle = null; + string? decompose = null; + string? decomposeTitle = null; if (mapping["decompose"] is JToken decomposeToken) { decompose = decomposeToken["value"]?.Value(); decomposeTitle = decomposeToken["titleCase"]?.Value(); } - + var accentsMappingData = new AccentsMappingData { Source = childName, @@ -71,6 +71,6 @@ public override bool CanConvert(Type objectType) } public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException(); } } diff --git a/Tests/Diacritics.Tests/Import/Model/Metadata.cs b/Tests/Diacritics.Tests/Import/Model/Metadata.cs index fb8d61e..fdb1fda 100644 --- a/Tests/Diacritics.Tests/Import/Model/Metadata.cs +++ b/Tests/Diacritics.Tests/Import/Model/Metadata.cs @@ -5,13 +5,21 @@ namespace Diacritics.Tests.Import { public class Metadata { - public string Alphabet { get; set; } + public Metadata() + { + this.Continents = new List(); + } + + [JsonProperty("alphabet")] + public string? Alphabet { get; set; } [JsonProperty("continent")] public ICollection Continents { get; set; } - public string Language { get; set; } + [JsonProperty("language")] + public string? Language { get; set; } - public string LanguageNative { get; set; } + [JsonProperty("languageNative")] + public string? LanguageNative { get; set; } } } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 004ed35..7ca65d2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,7 +7,7 @@ name: $[format('{0}', variables['buildName'])] pool: - vmImage: 'windows-2022' + vmImage: 'windows-latest' trigger: branches: @@ -25,7 +25,7 @@ variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' majorVersion: 4 - minorVersion: 0 + minorVersion: 1 patchVersion: $[counter(format('{0}.{1}', variables.majorVersion, variables.minorVersion), 0)] ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: # Versioning: 1.0.0 From 613f94088c8b3c85da6336aaa7502b9b9e7d669c Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Fri, 5 Dec 2025 09:10:50 +0100 Subject: [PATCH 2/6] Update editorconfig to latest version --- .editorconfig | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index b2d7e6f..356a1bb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ #################################################################### -# Editor Configuration (Updated 2023-07-26) +# Editor Configuration (Updated 2025-12-05) # -# (c)2023 superdev GmbH +# (c)2025 superdev GmbH #################################################################### root = true @@ -15,7 +15,7 @@ tab_width = 2 indent_style = space indent_size = 4 tab_width = 4 -end_of_line = crlf +end_of_line = lf trim_trailing_whitespace = true insert_final_newline = false max_line_length = 200 @@ -112,6 +112,7 @@ dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = false:silent dotnet_style_namespace_match_folder = true:suggestion dotnet_style_operator_placement_when_wrapping = beginning_of_line csharp_style_unused_value_expression_statement_preference = discard_variable:none @@ -175,4 +176,7 @@ dotnet_diagnostic.IDE0051.severity = warning dotnet_diagnostic.CS1591.severity = none # CA1416: Validate platform compatibility -dotnet_diagnostic.CA1416.severity = suggestion \ No newline at end of file +dotnet_diagnostic.CA1416.severity = suggestion + +# LocalVariableHidesMember: Local variable hides member +resharper_local_variable_hides_member_highlighting = none From 386c830b0d216d767f5e01cd28997fdafe09c228 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Fri, 5 Dec 2025 09:11:03 +0100 Subject: [PATCH 3/6] Remove obsolete class --- Diacritics/StaticDiacritics.cs | 30 ------------------------------ ReleaseNotes.txt | 1 + 2 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 Diacritics/StaticDiacritics.cs diff --git a/Diacritics/StaticDiacritics.cs b/Diacritics/StaticDiacritics.cs deleted file mode 100644 index 3f019a2..0000000 --- a/Diacritics/StaticDiacritics.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Threading; - -namespace Diacritics -{ - [Obsolete("Use DiacriticsMapper instead")] - public static class StaticDiacritics - { - private static Lazy Implementation; - - static StaticDiacritics() - { - SetDefaultMapper(CreateDefaultDiacriticsMapper); - } - - [Obsolete("Use DiacriticsMapper.Current instead")] - public static IDiacriticsMapper Current => Implementation.Value; - - [Obsolete("Use DiacriticsMapper.SetDefaultMapper instead")] - public static void SetDefaultMapper(Func factory) - { - Implementation = new Lazy(factory, LazyThreadSafetyMode.PublicationOnly); - } - - private static IDiacriticsMapper CreateDefaultDiacriticsMapper() - { - return new DefaultDiacriticsMapper(); - } - } -} \ No newline at end of file diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index c2c2cb0..7660ff3 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,5 +1,6 @@ 4.1 - Support for nullable reference types. +- Replace StaticDiacritics class with IDiacriticsMapper.Current. 4.0 - Use cached StringBuilder instances to improve memory usage and reduce garbage collection pressure. From 0180c725af79559e96ce8949d7080cb69fba5ad4 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Fri, 5 Dec 2025 09:11:20 +0100 Subject: [PATCH 4/6] Extend abbreviations list --- Diacritics.sln.DotSettings | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Diacritics.sln.DotSettings diff --git a/Diacritics.sln.DotSettings b/Diacritics.sln.DotSettings new file mode 100644 index 0000000..c60a7bc --- /dev/null +++ b/Diacritics.sln.DotSettings @@ -0,0 +1,7 @@ + + True + True + True + True + True + True \ No newline at end of file From 133e13416090bf28a6fc63a310871bc9c0f7534c Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Fri, 5 Dec 2025 09:11:28 +0100 Subject: [PATCH 5/6] Cleanup --- Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs b/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs index 2d368f0..6286ad8 100644 --- a/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs +++ b/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs @@ -37,7 +37,7 @@ public async Task GenerateAccentMappings(string languageUrl, string className) // Generate mapping file var fileContent = GenerateTemplate(className, mappings); var filePath = Path.Combine(AccentMappingsFolder, className + ".cs"); - File.WriteAllText(filePath, fileContent); + await File.WriteAllTextAsync(filePath, fileContent); } internal class ImportUrls : TheoryData From 12ad2c8ea86e332f1f219ebb0a3c98fc642b8581 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Fri, 5 Dec 2025 09:17:45 +0100 Subject: [PATCH 6/6] Fix nullability issues in unit tests --- Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs | 2 +- .../Import/Model/AccentsMappingDataJsonConverter.cs | 4 ++-- Tests/Diacritics.Tests/Internals/StringBuilderCacheTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs b/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs index 6286ad8..7763695 100644 --- a/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs +++ b/Tests/Diacritics.Tests/Import/ImportCodeGeneratorTests.cs @@ -86,7 +86,7 @@ private static async Task ImportAccentMappingsAsync(string url) using (var httpClient = new HttpClient()) { var json = await httpClient.GetStringAsync(url); - return JsonConvert.DeserializeObject(json); + return JsonConvert.DeserializeObject(json)!; } } diff --git a/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs b/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs index 8dd63a8..9a530df 100644 --- a/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs +++ b/Tests/Diacritics.Tests/Import/Model/AccentsMappingDataJsonConverter.cs @@ -35,14 +35,14 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis var mapping = childValue["mapping"]; string? @base = null; - if (mapping["base"] is JToken baseToken) + if (mapping?["base"] is JToken baseToken) { @base = baseToken.Value(); } string? decompose = null; string? decomposeTitle = null; - if (mapping["decompose"] is JToken decomposeToken) + if (mapping?["decompose"] is JToken decomposeToken) { decompose = decomposeToken["value"]?.Value(); decomposeTitle = decomposeToken["titleCase"]?.Value(); diff --git a/Tests/Diacritics.Tests/Internals/StringBuilderCacheTests.cs b/Tests/Diacritics.Tests/Internals/StringBuilderCacheTests.cs index 108f805..3cdf221 100644 --- a/Tests/Diacritics.Tests/Internals/StringBuilderCacheTests.cs +++ b/Tests/Diacritics.Tests/Internals/StringBuilderCacheTests.cs @@ -107,7 +107,7 @@ public void Cache_ShouldBeThreadLocal() stringBuilder1.Append("main"); StringBuilderCache.GetStringAndRelease(stringBuilder1); - StringBuilder stringBuilder2 = null; + StringBuilder? stringBuilder2 = null; // Act var thread = new Thread(() =>