< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 114
Coverable lines: 114
Total lines: 218
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 57
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)0%220%
.ctor(...)100%110%
ConvertName(...)100%110%
ConvertNameCore(...)0%51510%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Text.Json/Common/JsonSeparatorNamingPolicy.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Diagnostics;
 6using System.Globalization;
 7using System.Runtime.CompilerServices;
 8
 9namespace System.Text.Json
 10{
 11    internal abstract class JsonSeparatorNamingPolicy : JsonNamingPolicy
 12    {
 13        private readonly char? _separator;
 14        private readonly WordCasing _wordCasing;
 15
 016        internal JsonSeparatorNamingPolicy(bool lowercase, char separator)
 017        {
 018            Debug.Assert(char.IsPunctuation(separator));
 19
 020            _separator = separator;
 021            _wordCasing = lowercase ? WordCasing.LowerCase : WordCasing.UpperCase;
 022        }
 23
 024        internal JsonSeparatorNamingPolicy(WordCasing wordCasing)
 025        {
 026            Debug.Assert(wordCasing is WordCasing.PascalCase);
 27
 028            _separator = null;
 029            _wordCasing = wordCasing;
 030        }
 31
 32        public sealed override string ConvertName(string name)
 033        {
 034            ArgumentNullException.ThrowIfNull(name);
 35
 036            return ConvertNameCore(_separator, _wordCasing, name.AsSpan());
 037        }
 38
 39        private static string ConvertNameCore(char? separator, WordCasing wordCasing, ReadOnlySpan<char> chars)
 040        {
 041            char[]? rentedBuffer = null;
 42
 43            // While we can't predict the expansion factor of the resultant string,
 44            // start with a buffer that is at least 20% larger than the input.
 045            int initialBufferLength = (int)(1.2 * chars.Length);
 046            Span<char> destination = initialBufferLength <= JsonConstants.StackallocCharThreshold
 047                ? stackalloc char[JsonConstants.StackallocCharThreshold]
 048                : (rentedBuffer = ArrayPool<char>.Shared.Rent(initialBufferLength));
 49
 050            SeparatorState state = SeparatorState.NotStarted;
 051            int charsWritten = 0;
 52
 053            for (int i = 0; i < chars.Length; i++)
 054            {
 55                // NB this implementation does not handle surrogate pair letters
 56                // cf. https://github.com/dotnet/runtime/issues/90352
 57
 058                char current = chars[i];
 059                UnicodeCategory category = char.GetUnicodeCategory(current);
 60
 061                switch (category)
 62                {
 63                    case UnicodeCategory.UppercaseLetter:
 64
 065                        bool isWordBoundary = false;
 66
 067                        switch (state)
 68                        {
 69                            case SeparatorState.NotStarted:
 070                                isWordBoundary = true;
 071                                break;
 72
 73                            case SeparatorState.LowercaseLetterOrDigit:
 74                            case SeparatorState.SpaceSeparator:
 75                                // An uppercase letter following a sequence of lowercase letters or spaces
 76                                // denotes the start of a new grouping: emit a separator character.
 077                                isWordBoundary = true;
 078                                if (separator.HasValue)
 079                                {
 080                                    WriteChar(separator.Value, ref destination);
 081                                }
 082                                break;
 83
 84                            case SeparatorState.UppercaseLetter:
 85                                // We are reading through a sequence of two or more uppercase letters.
 86                                // Uppercase letters are grouped together with the exception of the
 87                                // final letter, assuming it is followed by lowercase letters.
 88                                // For example, the value 'XMLReader' should render as 'xml_reader',
 89                                // however 'SHA512Hash' should render as 'sha512-hash'.
 090                                if (i + 1 < chars.Length && char.IsLower(chars[i + 1]))
 091                                {
 092                                    isWordBoundary = true;
 093                                    if (separator.HasValue)
 094                                    {
 095                                        WriteChar(separator.Value, ref destination);
 096                                    }
 097                                }
 098                                break;
 99
 100                            default:
 0101                                Debug.Fail($"Unexpected state {state}");
 102                                break;
 103                        }
 104
 0105                        current = wordCasing switch
 0106                        {
 0107                            WordCasing.LowerCase => char.ToLowerInvariant(current),
 0108                            WordCasing.PascalCase => isWordBoundary ? current : char.ToLowerInvariant(current),
 0109                            _ => current,
 0110                        };
 111
 0112                        WriteChar(current, ref destination);
 0113                        state = SeparatorState.UppercaseLetter;
 0114                        break;
 115
 116                    case UnicodeCategory.LowercaseLetter:
 117                    case UnicodeCategory.DecimalDigitNumber:
 118
 0119                        bool isWordStart = state is SeparatorState.SpaceSeparator or SeparatorState.NotStarted;
 120
 0121                        if (state is SeparatorState.SpaceSeparator)
 0122                        {
 123                            // Normalize preceding spaces to one separator.
 0124                            if (separator.HasValue)
 0125                            {
 0126                                WriteChar(separator.Value, ref destination);
 0127                            }
 0128                        }
 129
 0130                        if (category is UnicodeCategory.LowercaseLetter)
 0131                        {
 0132                            current = wordCasing switch
 0133                            {
 0134                                WordCasing.UpperCase => char.ToUpperInvariant(current),
 0135                                WordCasing.PascalCase => isWordStart ? char.ToUpperInvariant(current) : current,
 0136                                _ => current,
 0137                            };
 0138                        }
 139
 0140                        WriteChar(current, ref destination);
 0141                        state = SeparatorState.LowercaseLetterOrDigit;
 0142                        break;
 143
 144                    case UnicodeCategory.SpaceSeparator:
 145                        // Space characters are trimmed from the start and end of the input string
 146                        // but are normalized to separator characters if between letters.
 0147                        if (state != SeparatorState.NotStarted)
 0148                        {
 0149                            state = SeparatorState.SpaceSeparator;
 0150                        }
 0151                        break;
 152
 153                    default:
 154                        // Non-alphanumeric characters (including the separator character and surrogates)
 155                        // are written as-is to the output and reset the separator state.
 156                        // E.g. 'ABC???def' maps to 'abc???def' in snake_case.
 157
 0158                        WriteChar(current, ref destination);
 0159                        state = SeparatorState.NotStarted;
 0160                        break;
 161                }
 0162            }
 163
 0164            string result = destination.Slice(0, charsWritten).ToString();
 165
 0166            if (rentedBuffer is not null)
 0167            {
 0168                destination.Slice(0, charsWritten).Clear();
 0169                ArrayPool<char>.Shared.Return(rentedBuffer);
 0170            }
 171
 0172            return result;
 173
 174            [MethodImpl(MethodImplOptions.AggressiveInlining)]
 175            void WriteChar(char value, ref Span<char> destination)
 0176            {
 0177                if (charsWritten == destination.Length)
 0178                {
 0179                    ExpandBuffer(ref destination);
 0180                }
 181
 0182                destination[charsWritten++] = value;
 0183            }
 184
 185            void ExpandBuffer(ref Span<char> destination)
 0186            {
 0187                int newSize = checked(destination.Length * 2);
 0188                char[] newBuffer = ArrayPool<char>.Shared.Rent(newSize);
 0189                destination.CopyTo(newBuffer);
 190
 0191                if (rentedBuffer is not null)
 0192                {
 0193                    destination.Slice(0, charsWritten).Clear();
 0194                    ArrayPool<char>.Shared.Return(rentedBuffer);
 0195                }
 196
 0197                rentedBuffer = newBuffer;
 0198                destination = rentedBuffer;
 0199            }
 0200        }
 201
 202        private enum SeparatorState
 203        {
 204            NotStarted,
 205            UppercaseLetter,
 206            LowercaseLetterOrDigit,
 207            SpaceSeparator,
 208        }
 209
 210        internal enum WordCasing
 211        {
 212            LowerCase,
 213            UpperCase,
 214            PascalCase,
 215        }
 216    }
 217}
 218