< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 109
Coverable lines: 109
Total lines: 355
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 68
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%880%
Parse(...)0%220%
ValidateNumberOfArgs(...)0%220%
TryParseLiterals(...)0%54540%
TryMoveNext(System.ReadOnlySpan`1<System.Char>,System.Int32&,System.Char&)0%220%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Private.CoreLib/src/System/Text/CompositeFormat.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.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Runtime.CompilerServices;
 8
 9namespace System.Text
 10{
 11    /// <summary>Represents a parsed composite format string.</summary>
 12    [DebuggerDisplay("{Format}")]
 13    public sealed class CompositeFormat
 14    {
 15        /// <summary>The parsed segments that make up the composite format string.</summary>
 16        /// <remarks>
 17        /// Every segment represents either a literal or a format hole, based on whether Literal
 18        /// is non-null or ArgIndex is non-negative.
 19        /// </remarks>
 20        internal readonly (string? Literal, int ArgIndex, int Alignment, string? Format)[] _segments;
 21        /// <summary>The sum of the lengths of all of the literals in <see cref="_segments"/>.</summary>
 22        internal readonly int _literalLength;
 23        /// <summary>The number of segments in <see cref="_segments"/> that represent format holes.</summary>
 24        internal readonly int _formattedCount;
 25        /// <summary>The number of args required to satisfy the format holes.</summary>
 26        /// <remarks>This is equal to one more than the largest index required by any format hole.</remarks>
 27        internal readonly int _argsRequired;
 28
 29        /// <summary>Initializes the instance.</summary>
 30        /// <param name="format">The composite format string that was parsed.</param>
 31        /// <param name="segments">The parsed segments.</param>
 032        private CompositeFormat(string format, (string? Literal, int ArgIndex, int Alignment, string? Format)[] segments
 33        {
 34            // Store the format.
 035            Debug.Assert(format is not null);
 036            Format = format;
 37
 38            // Store the segments.
 039            Debug.Assert(segments is not null);
 040            _segments = segments;
 41
 42            // Compute derivative information from the segments.
 043            int literalLength = 0, formattedCount = 0, argsRequired = 0;
 044            foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in segments)
 45            {
 046                Debug.Assert((segment.Literal is not null) ^ (segment.ArgIndex >= 0), "The segment should represent a li
 47
 048                if (segment.Literal is string literal)
 49                {
 050                    literalLength += literal.Length; // no concern about overflow as these were parsed out of a single s
 51                }
 052                else if (segment.ArgIndex >= 0)
 53                {
 054                    formattedCount++;
 055                    argsRequired = Math.Max(argsRequired, segment.ArgIndex + 1);
 56                }
 57            }
 58
 59            // Store the derivative information.
 060            Debug.Assert(literalLength >= 0);
 061            Debug.Assert(formattedCount >= 0);
 062            Debug.Assert(formattedCount == 0 || argsRequired > 0);
 063            _literalLength = literalLength;
 064            _formattedCount = formattedCount;
 065            _argsRequired = argsRequired;
 066        }
 67
 68        /// <summary>Parse the composite format string <paramref name="format"/>.</summary>
 69        /// <param name="format">The string to parse.</param>
 70        /// <returns>The parsed <see cref="CompositeFormat"/>.</returns>
 71        /// <exception cref="ArgumentNullException"><paramref name="format"/> is null.</exception>
 72        /// <exception cref="FormatException">A format item in <paramref name="format"/> is invalid.</exception>
 73        public static CompositeFormat Parse([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format)
 74        {
 075            ArgumentNullException.ThrowIfNull(format);
 76
 077            var segments = new List<(string? Literal, int ArgIndex, int Alignment, string? Format)>();
 078            int failureOffset = default;
 079            ExceptionResource failureReason = default;
 080            if (!TryParseLiterals(format, segments, ref failureOffset, ref failureReason))
 81            {
 082                ThrowHelper.ThrowFormatInvalidString(failureOffset, failureReason);
 83            }
 84
 085            return new CompositeFormat(format, segments.ToArray());
 86        }
 87
 88        /// <summary>Gets the original composite format string used to create this <see cref="CompositeFormat"/> instanc
 089        public string Format { get; }
 90
 91        /// <summary>Gets the minimum number of arguments that must be passed to a formatting operation using this <see 
 92        /// <remarks>It's permissible to supply more arguments than this value, but it's an error to pass fewer.</remark
 093        public int MinimumArgumentCount => _argsRequired;
 94
 95        /// <summary>Throws an exception if the specified number of arguments is fewer than the number required.</summar
 96        /// <param name="numArgs">The number of arguments provided by the caller.</param>
 97        /// <exception cref="FormatException">An insufficient number of arguments were provided.</exception>
 98        internal void ValidateNumberOfArgs(int numArgs)
 99        {
 0100            if (numArgs < _argsRequired)
 101            {
 0102                ThrowHelper.ThrowFormatIndexOutOfRange();
 103            }
 0104        }
 105
 106        /// <summary>Parse the composite format string into segments.</summary>
 107        /// <param name="format">The format string.</param>
 108        /// <param name="segments">The list into which to store the segments.</param>
 109        /// <param name="failureOffset">The offset at which a parsing error occurred if <see langword="false"/> is retur
 110        /// <param name="failureReason">The reason for a parsing failure if <see langword="false"/> is returned.</param>
 111        /// <returns>true if the format string can be parsed successfully; otherwise, false.</returns>
 112        private static unsafe bool TryParseLiterals(ReadOnlySpan<char> format, List<(string? Literal, int ArgIndex, int 
 113        {
 114            // This parsing logic is copied from string.Format.  It's the same code modified to not format
 115            // as part of parsing and instead store the parsed literals and argument specifiers (alignment
 116            // and format) for later use.
 117
 118            // Rather than parsing directly into the segments list, literals are parsed into a reusable builder.
 119            // Due to the nature of the parsing logic copied from string.Format, and our desire not to veer from
 120            // it significantly in order to maintain compatibility and accidental regression, multiple literals
 121            // next to each other might be parsed separately due to braces in between them.  This builder then
 122            // allows us to merge those segments back together easily prior to their being appended to the list.
 0123            var vsb = new ValueStringBuilder(stackalloc char[string.StackallocCharBufferSizeLimit]);
 124
 125            // Repeatedly find the next hole and process it.
 0126            int pos = 0;
 127            char ch;
 0128            while (true)
 129            {
 130                // Skip until either the end of the input or the first unescaped opening brace, whichever comes first.
 131                // Along the way we need to also unescape escaped closing braces.
 132                while (true)
 133                {
 134                    // Find the next brace.  If there isn't one, the remainder of the input is text to be appended, and 
 0135                    ReadOnlySpan<char> remainder = format.Slice(pos);
 0136                    int countUntilNextBrace = remainder.IndexOfAny('{', '}');
 0137                    if (countUntilNextBrace < 0)
 138                    {
 0139                        vsb.Append(remainder);
 0140                        segments.Add((vsb.ToString(), -1, 0, null));
 0141                        return true;
 142                    }
 143
 144                    // Append the text until the brace.
 0145                    vsb.Append(remainder.Slice(0, countUntilNextBrace));
 0146                    pos += countUntilNextBrace;
 147
 148                    // Get the brace.  It must be followed by another character, either a copy of itself in the case of 
 149                    // escaped, or an arbitrary character that's part of the hole in the case of an opening brace.
 0150                    char brace = format[pos];
 0151                    if (!TryMoveNext(format, ref pos, out ch))
 152                    {
 153                        goto FailureUnclosedFormatItem;
 154                    }
 0155                    if (brace == ch)
 156                    {
 0157                        vsb.Append(ch);
 0158                        pos++;
 0159                        continue;
 160                    }
 161
 162                    // This wasn't an escape, so it must be an opening brace.
 0163                    if (brace != '{')
 164                    {
 165                        goto FailureUnexpectedClosingBrace;
 166                    }
 167
 168                    // Proceed to parse the hole.
 0169                    segments.Add((vsb.ToString(), -1, 0, null));
 0170                    vsb.Length = 0;
 171                    break;
 172                }
 173
 174                // We're now positioned just after the opening brace of an argument hole, which consists of
 175                // an opening brace, an index, an optional width preceded by a comma, and an optional format
 176                // preceded by a colon, with arbitrary amounts of spaces throughout.
 0177                int width = 0;
 0178                string? itemFormat = null; // used if itemFormat is null
 179
 180                // First up is the index parameter, which is of the form:
 181                //     at least on digit
 182                //     optional any number of spaces
 183                // We've already read the first digit into ch.
 0184                Debug.Assert(format[pos - 1] == '{');
 0185                Debug.Assert(ch != '{');
 0186                int index = ch - '0';
 0187                if ((uint)index >= 10u)
 188                {
 189                    goto FailureExpectedAsciiDigit;
 190                }
 191
 192                // Common case is a single digit index followed by a closing brace.  If it's not a closing brace,
 193                // proceed to finish parsing the full hole format.
 0194                if (!TryMoveNext(format, ref pos, out ch))
 195                {
 196                    goto FailureUnclosedFormatItem;
 197                }
 0198                if (ch != '}')
 199                {
 200                    // Continue consuming optional additional digits.
 0201                    while (char.IsAsciiDigit(ch))
 202                    {
 0203                        index = index * 10 + ch - '0';
 0204                        if (!TryMoveNext(format, ref pos, out ch))
 205                        {
 206                            goto FailureUnclosedFormatItem;
 207                        }
 208                    }
 209
 210                    // Consume optional whitespace.
 0211                    while (ch == ' ')
 212                    {
 0213                        if (!TryMoveNext(format, ref pos, out ch))
 214                        {
 215                            goto FailureUnclosedFormatItem;
 216                        }
 217                    }
 218
 219                    // Parse the optional alignment, which is of the form:
 220                    //     comma
 221                    //     optional any number of spaces
 222                    //     optional -
 223                    //     at least one digit
 224                    //     optional any number of spaces
 0225                    if (ch == ',')
 226                    {
 227                        // Consume optional whitespace.
 228                        do
 229                        {
 0230                            if (!TryMoveNext(format, ref pos, out ch))
 231                            {
 232                                goto FailureUnclosedFormatItem;
 233                            }
 234                        }
 0235                        while (ch == ' ');
 236
 237                        // Consume an optional minus sign indicating left alignment.
 0238                        int leftJustify = 1;
 0239                        if (ch == '-')
 240                        {
 0241                            leftJustify = -1;
 0242                            if (!TryMoveNext(format, ref pos, out ch))
 243                            {
 244                                goto FailureUnclosedFormatItem;
 245                            }
 246                        }
 247
 248                        // Parse alignment digits. The read character must be a digit.
 0249                        width = ch - '0';
 0250                        if ((uint)width >= 10u)
 251                        {
 252                            goto FailureExpectedAsciiDigit;
 253                        }
 0254                        if (!TryMoveNext(format, ref pos, out ch))
 255                        {
 0256                            goto FailureUnclosedFormatItem;
 257                        }
 0258                        while (char.IsAsciiDigit(ch))
 259                        {
 0260                            width = width * 10 + ch - '0';
 0261                            if (!TryMoveNext(format, ref pos, out ch))
 262                            {
 263                                goto FailureUnclosedFormatItem;
 264                            }
 265                        }
 0266                        width *= leftJustify;
 267
 268                        // Consume optional whitespace
 0269                        while (ch == ' ')
 270                        {
 0271                            if (!TryMoveNext(format, ref pos, out ch))
 272                            {
 273                                goto FailureUnclosedFormatItem;
 274                            }
 275                        }
 276                    }
 277
 278                    // The next character needs to either be a closing brace for the end of the hole,
 279                    // or a colon indicating the start of the format.
 0280                    if (ch != '}')
 281                    {
 0282                        if (ch != ':')
 283                        {
 284                            // Unexpected character
 285                            goto FailureUnclosedFormatItem;
 286                        }
 287
 288                        // Search for the closing brace; everything in between is the format,
 289                        // but opening braces aren't allowed.
 0290                        int startingPos = pos;
 291                        while (true)
 292                        {
 0293                            if (!TryMoveNext(format, ref pos, out ch))
 294                            {
 295                                goto FailureUnclosedFormatItem;
 296                            }
 297
 0298                            if (ch == '}')
 299                            {
 300                                // Argument hole closed
 301                                break;
 302                            }
 303
 0304                            if (ch == '{')
 305                            {
 306                                // Braces inside the argument hole are not supported
 0307                                goto FailureUnclosedFormatItem;
 308                            }
 309                        }
 310
 0311                        startingPos++;
 0312                        itemFormat = format.Slice(startingPos, pos - startingPos).ToString();
 313                    }
 314                }
 315
 0316                Debug.Assert(format[pos] == '}');
 0317                pos++;
 318
 0319                segments.Add((null, index, width, itemFormat));
 320
 321                // Continue parsing the rest of the format string.
 322            }
 323
 324            FailureUnexpectedClosingBrace:
 0325            failureReason = ExceptionResource.Format_UnexpectedClosingBrace;
 0326            failureOffset = pos;
 0327            return false;
 328
 329            FailureUnclosedFormatItem:
 0330            failureReason = ExceptionResource.Format_UnclosedFormatItem;
 0331            failureOffset = pos;
 0332            return false;
 333
 334            FailureExpectedAsciiDigit:
 0335            failureReason = ExceptionResource.Format_ExpectedAsciiDigit;
 0336            failureOffset = pos;
 0337            return false;
 338
 339            [MethodImpl(MethodImplOptions.AggressiveInlining)]
 340            static bool TryMoveNext(ReadOnlySpan<char> format, ref int pos, out char nextChar)
 341            {
 0342                pos++;
 0343                if ((uint)pos >= (uint)format.Length)
 344                {
 0345                    nextChar = '\0';
 0346                    return false;
 347                }
 348
 0349                nextChar = format[pos];
 0350                return true;
 351            }
 352        }
 353    }
 354}
 355