Skip to content

Commit 41a1695

Browse files
committed
Standardize AppendFormat between StringBuilder and ValueStringBuilder
1 parent 1094879 commit 41a1695

6 files changed

Lines changed: 313 additions & 540 deletions

File tree

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,7 @@
12161216
<Compile Include="$(MSBuildThisFileDirectory)System\Text\SpanRuneEnumerator.cs" />
12171217
<Compile Include="$(MSBuildThisFileDirectory)System\Text\StringBuilder.cs" />
12181218
<Compile Include="$(MSBuildThisFileDirectory)System\Text\StringBuilder.Debug.cs" Condition="'$(Configuration)' == 'Debug'" />
1219+
<Compile Include="$(MSBuildThisFileDirectory)System\Text\StringBuilderInternal.cs" />
12191220
<Compile Include="$(MSBuildThisFileDirectory)System\Text\StringRuneEnumerator.cs" />
12201221
<Compile Include="$(MSBuildThisFileDirectory)System\Text\TranscodingStream.cs" />
12211222
<Compile Include="$(MSBuildThisFileDirectory)System\Text\TrimType.cs" />

src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ private void WriteFormatHelper(string format, ReadOnlySpan<object?> args, bool a
505505
new ValueStringBuilder(stackalloc char[256]) :
506506
new ValueStringBuilder(estimatedLength);
507507

508-
vsb.AppendFormatHelper(null, format!, args); // AppendFormatHelper will appropriately throw ArgumentNullException for a null format
508+
vsb.AppendFormat(null, format!, args); // AppendFormatHelper will appropriately throw ArgumentNullException for a null format
509509

510510
WriteSpan(vsb.AsSpan(), appendNewLine);
511511

src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ private static string FormatHelper(IFormatProvider? provider, string format, Rea
540540

541541
var sb = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]);
542542
sb.EnsureCapacity(format.Length + args.Length * 8);
543-
sb.AppendFormatHelper(provider, format, args);
543+
sb.AppendFormat(provider, format, args);
544544
return sb.ToString();
545545
}
546546

src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs

Lines changed: 10 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace System.Text
2121
// class to carry out modifications upon strings.
2222
[Serializable]
2323
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
24-
public sealed partial class StringBuilder : ISerializable
24+
public sealed partial class StringBuilder : ISerializable, IStringBuilderInternal
2525
{
2626
// A StringBuilder is internally represented as a linked list of blocks each of which holds
2727
// a chunk of the string. It turns out string as a whole can also be represented as just a chunk,
@@ -1518,274 +1518,9 @@ public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(Strin
15181518
/// </exception>
15191519
public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args)
15201520
{
1521-
ArgumentNullException.ThrowIfNull(format);
1522-
1523-
// Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment.
1524-
const int IndexLimit = 1_000_000; // Note: 0 <= ArgIndex < IndexLimit
1525-
const int WidthLimit = 1_000_000; // Note: -WidthLimit < ArgAlign < WidthLimit
1526-
1527-
// Query the provider (if one was supplied) for an ICustomFormatter. If there is one,
1528-
// it needs to be used to transform all arguments.
1529-
ICustomFormatter? cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter));
1530-
1531-
// Repeatedly find the next hole and process it.
1532-
int pos = 0;
1533-
char ch;
1534-
while (true)
1535-
{
1536-
// Skip until either the end of the input or the first unescaped opening brace, whichever comes first.
1537-
// Along the way we need to also unescape escaped closing braces.
1538-
while (true)
1539-
{
1540-
// Find the next brace. If there isn't one, the remainder of the input is text to be appended, and we're done.
1541-
if ((uint)pos >= (uint)format.Length)
1542-
{
1543-
return this;
1544-
}
1545-
1546-
ReadOnlySpan<char> remainder = format.AsSpan(pos);
1547-
int countUntilNextBrace = remainder.IndexOfAny('{', '}');
1548-
if (countUntilNextBrace < 0)
1549-
{
1550-
Append(remainder);
1551-
return this;
1552-
}
1553-
1554-
// Append the text until the brace.
1555-
Append(remainder.Slice(0, countUntilNextBrace));
1556-
pos += countUntilNextBrace;
1557-
1558-
// Get the brace. It must be followed by another character, either a copy of itself in the case of being
1559-
// escaped, or an arbitrary character that's part of the hole in the case of an opening brace.
1560-
char brace = format[pos];
1561-
ch = MoveNext(format, ref pos);
1562-
if (brace == ch)
1563-
{
1564-
Append(ch);
1565-
pos++;
1566-
continue;
1567-
}
1568-
1569-
// This wasn't an escape, so it must be an opening brace.
1570-
if (brace != '{')
1571-
{
1572-
ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnexpectedClosingBrace);
1573-
}
1574-
1575-
// Proceed to parse the hole.
1576-
break;
1577-
}
1578-
1579-
// We're now positioned just after the opening brace of an argument hole, which consists of
1580-
// an opening brace, an index, an optional width preceded by a comma, and an optional format
1581-
// preceded by a colon, with arbitrary amounts of spaces throughout.
1582-
int width = 0;
1583-
bool leftJustify = false;
1584-
ReadOnlySpan<char> itemFormatSpan = default; // used if itemFormat is null
1585-
1586-
// First up is the index parameter, which is of the form:
1587-
// at least on digit
1588-
// optional any number of spaces
1589-
// We've already read the first digit into ch.
1590-
Debug.Assert(format[pos - 1] == '{');
1591-
Debug.Assert(ch != '{');
1592-
int index = ch - '0';
1593-
if ((uint)index >= 10u)
1594-
{
1595-
ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit);
1596-
}
1597-
1598-
// Common case is a single digit index followed by a closing brace. If it's not a closing brace,
1599-
// proceed to finish parsing the full hole format.
1600-
ch = MoveNext(format, ref pos);
1601-
if (ch != '}')
1602-
{
1603-
// Continue consuming optional additional digits.
1604-
while (char.IsAsciiDigit(ch) && index < IndexLimit)
1605-
{
1606-
index = index * 10 + ch - '0';
1607-
ch = MoveNext(format, ref pos);
1608-
}
1609-
1610-
// Consume optional whitespace.
1611-
while (ch == ' ')
1612-
{
1613-
ch = MoveNext(format, ref pos);
1614-
}
1615-
1616-
// Parse the optional alignment, which is of the form:
1617-
// comma
1618-
// optional any number of spaces
1619-
// optional -
1620-
// at least one digit
1621-
// optional any number of spaces
1622-
if (ch == ',')
1623-
{
1624-
// Consume optional whitespace.
1625-
do
1626-
{
1627-
ch = MoveNext(format, ref pos);
1628-
}
1629-
while (ch == ' ');
1630-
1631-
// Consume an optional minus sign indicating left alignment.
1632-
if (ch == '-')
1633-
{
1634-
leftJustify = true;
1635-
ch = MoveNext(format, ref pos);
1636-
}
1637-
1638-
// Parse alignment digits. The read character must be a digit.
1639-
width = ch - '0';
1640-
if ((uint)width >= 10u)
1641-
{
1642-
ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_ExpectedAsciiDigit);
1643-
}
1644-
ch = MoveNext(format, ref pos);
1645-
while (char.IsAsciiDigit(ch) && width < WidthLimit)
1646-
{
1647-
width = width * 10 + ch - '0';
1648-
ch = MoveNext(format, ref pos);
1649-
}
1650-
1651-
// Consume optional whitespace
1652-
while (ch == ' ')
1653-
{
1654-
ch = MoveNext(format, ref pos);
1655-
}
1656-
}
1657-
1658-
// The next character needs to either be a closing brace for the end of the hole,
1659-
// or a colon indicating the start of the format.
1660-
if (ch != '}')
1661-
{
1662-
if (ch != ':')
1663-
{
1664-
// Unexpected character
1665-
ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem);
1666-
}
1667-
1668-
// Search for the closing brace; everything in between is the format,
1669-
// but opening braces aren't allowed.
1670-
int startingPos = pos;
1671-
while (true)
1672-
{
1673-
ch = MoveNext(format, ref pos);
1674-
1675-
if (ch == '}')
1676-
{
1677-
// Argument hole closed
1678-
break;
1679-
}
1680-
1681-
if (ch == '{')
1682-
{
1683-
// Braces inside the argument hole are not supported
1684-
ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem);
1685-
}
1686-
}
1687-
1688-
startingPos++;
1689-
itemFormatSpan = format.AsSpan(startingPos, pos - startingPos);
1690-
}
1691-
}
1692-
1693-
// Construct the output for this arg hole.
1694-
Debug.Assert(format[pos] == '}');
1695-
pos++;
1696-
string? s = null;
1697-
string? itemFormat = null;
1698-
1699-
if ((uint)index >= (uint)args.Length)
1700-
{
1701-
ThrowHelper.ThrowFormatIndexOutOfRange();
1702-
}
1703-
object? arg = args[index];
1704-
1705-
if (cf != null)
1706-
{
1707-
if (!itemFormatSpan.IsEmpty)
1708-
{
1709-
itemFormat = new string(itemFormatSpan);
1710-
}
1711-
1712-
s = cf.Format(itemFormat, arg, provider);
1713-
}
1714-
1715-
if (s == null)
1716-
{
1717-
// If arg is ISpanFormattable and the beginning doesn't need padding,
1718-
// try formatting it into the remaining current chunk.
1719-
if ((leftJustify || width == 0) &&
1720-
arg is ISpanFormattable spanFormattableArg &&
1721-
spanFormattableArg.TryFormat(RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider))
1722-
{
1723-
if ((uint)charsWritten > (uint)RemainingCurrentChunk.Length)
1724-
{
1725-
// Untrusted ISpanFormattable implementations might return an erroneous charsWritten value,
1726-
// and m_ChunkLength might end up being used in Unsafe code, so fail if we get back an
1727-
// out-of-range charsWritten value.
1728-
ThrowHelper.ThrowFormatInvalidString();
1729-
}
1730-
1731-
m_ChunkLength += charsWritten;
1732-
1733-
// Pad the end, if needed.
1734-
if (leftJustify && width > charsWritten)
1735-
{
1736-
Append(' ', width - charsWritten);
1737-
}
1738-
1739-
// Continue to parse other characters.
1740-
continue;
1741-
}
1742-
1743-
// Otherwise, fallback to trying IFormattable or calling ToString.
1744-
if (arg is IFormattable formattableArg)
1745-
{
1746-
if (itemFormatSpan.Length != 0)
1747-
{
1748-
itemFormat ??= new string(itemFormatSpan);
1749-
}
1750-
s = formattableArg.ToString(itemFormat, provider);
1751-
}
1752-
else
1753-
{
1754-
s = arg?.ToString();
1755-
}
1756-
1757-
s ??= string.Empty;
1758-
}
1759-
1760-
// Append it to the final output of the Format String.
1761-
if (width <= s.Length)
1762-
{
1763-
Append(s);
1764-
}
1765-
else if (leftJustify)
1766-
{
1767-
Append(s);
1768-
Append(' ', width - s.Length);
1769-
}
1770-
else
1771-
{
1772-
Append(' ', width - s.Length);
1773-
Append(s);
1774-
}
1775-
1776-
// Continue parsing the rest of the format string.
1777-
}
1778-
1779-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1780-
static char MoveNext(string format, ref int pos)
1781-
{
1782-
pos++;
1783-
if ((uint)pos >= (uint)format.Length)
1784-
{
1785-
ThrowHelper.ThrowFormatInvalidString(pos, ExceptionResource.Format_UnclosedFormatItem);
1786-
}
1787-
return format[pos];
1788-
}
1521+
StringBuilder sb = this;
1522+
StringBuilderInternal.AppendFormatHelper(ref sb, provider, format, args);
1523+
return this;
17891524
}
17901525

17911526
/// <summary>
@@ -2798,6 +2533,12 @@ private void Remove(int startIndex, int count, out StringBuilder chunk, out int
27982533
AssertInvariants();
27992534
}
28002535

2536+
Span<char> IStringBuilderInternal.RemainingCurrentChunk => RemainingCurrentChunk;
2537+
void IStringBuilderInternal.Append(char item) => Append(item);
2538+
void IStringBuilderInternal.Append(char item, int count) => Append(item, count);
2539+
void IStringBuilderInternal.Append(ReadOnlySpan<char> value) => Append(value);
2540+
void IStringBuilderInternal.UnsafeGrow(int size) => m_ChunkLength += size;
2541+
28012542
/// <summary>Provides a handler used by the language compiler to append interpolated strings into <see cref="StringBuilder"/> instances.</summary>
28022543
[EditorBrowsable(EditorBrowsableState.Never)]
28032544
[InterpolatedStringHandler]

0 commit comments

Comments
 (0)