Skip to content

Commit

Permalink
Add System.Text.CompositeFormat (dotnet#80753)
Browse files Browse the repository at this point in the history
* Add System.Text.CompositeFormat

* Conditionalize a test and remove extra const

* Address PR feedback
  • Loading branch information
stephentoub authored and mdh1418 committed Jan 24, 2023
1 parent 9895a9e commit 87b9376
Show file tree
Hide file tree
Showing 27 changed files with 1,540 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public static bool IsTokenOfType(int token, params MetadataTokenType[] types)
public bool IsAssembly => TokenType == MetadataTokenType.Assembly;
public bool IsGenericPar => TokenType == MetadataTokenType.GenericPar;

public override string ToString() => string.Format(CultureInfo.InvariantCulture, "0x{0:x8}", Value);
public override string ToString() => string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"0x{Value:x8}");
}

internal unsafe struct MetadataEnumResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,18 @@ public override string ToString()
{
if (Flags == ExceptionHandlingClauseOptions.Clause)
{
return string.Format(CultureInfo.CurrentUICulture,
"Flags={0}, TryOffset={1}, TryLength={2}, HandlerOffset={3}, HandlerLength={4}, CatchType={5}",
Flags, TryOffset, TryLength, HandlerOffset, HandlerLength, CatchType);
return string.Create(CultureInfo.CurrentUICulture,
$"Flags={Flags}, TryOffset={TryOffset}, TryLength={TryLength}, HandlerOffset={HandlerOffset}, HandlerLength={HandlerLength}, CatchType={CatchType}");
}

if (Flags == ExceptionHandlingClauseOptions.Filter)
{
return string.Format(CultureInfo.CurrentUICulture,
"Flags={0}, TryOffset={1}, TryLength={2}, HandlerOffset={3}, HandlerLength={4}, FilterOffset={5}",
Flags, TryOffset, TryLength, HandlerOffset, HandlerLength, FilterOffset);
return string.Create(CultureInfo.CurrentUICulture,
$"Flags={Flags}, TryOffset={TryOffset}, TryLength={TryLength}, HandlerOffset={HandlerOffset}, HandlerLength={HandlerLength}, FilterOffset={FilterOffset}");
}

return string.Format(CultureInfo.CurrentUICulture,
"Flags={0}, TryOffset={1}, TryLength={2}, HandlerOffset={3}, HandlerLength={4}",
Flags, TryOffset, TryLength, HandlerOffset, HandlerLength);
return string.Create(CultureInfo.CurrentUICulture,
$"Flags={Flags}, TryOffset={TryOffset}, TryLength={TryLength}, HandlerOffset={HandlerOffset}, HandlerLength={HandlerLength}");
}
}
}
163 changes: 113 additions & 50 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2661,7 +2661,6 @@ public static IEnumerable<object[]> Format_Valid_TestData()
yield return new object[] { null, "Foo }}{0}", new object[] { 1 }, "Foo }1" }; // Escaped closed curly braces
yield return new object[] { null, "Foo {0} {{0}}", new object[] { 1 }, "Foo 1 {0}" }; // Escaped placeholder


yield return new object[] { null, "Foo {0}", new object[] { null }, "Foo " }; // Values has null only
yield return new object[] { null, "Foo {0} {1} {2}", new object[] { "Bar", null, "Baz" }, "Foo Bar Baz" }; // Values has null

Expand All @@ -2670,6 +2669,29 @@ public static IEnumerable<object[]> Format_Valid_TestData()
yield return new object[] { new CustomFormatter(), "{0}", new object[] { 1.2 }, "abc" }; // Custom format provider
yield return new object[] { new CustomFormatter(), "{0:0}", new object[] { 1.2 }, "abc" }; // Custom format provider

// More arguments than needed
yield return new object[] { null, "{0}", new object[] { 1, 2 }, "1" };
yield return new object[] { null, "{0}", new object[] { 1, 2, 3 }, "1" };
yield return new object[] { null, "{0}", new object[] { 1, 2, 3, 4 }, "1" };
yield return new object[] { null, "{0}{1}", new object[] { 1, 2, 3 }, "12" };
yield return new object[] { null, "{0}{1}", new object[] { 1, 2, 3, 4 }, "12" };
yield return new object[] { null, "{0}{1}", new object[] { 1, 2, 3, 4, 5 }, "12" };
yield return new object[] { null, "{0}{1}{2}", new object[] { 1, 2, 3, 4 }, "123" };
yield return new object[] { null, "{0}{1}{2}", new object[] { 1, 2, 3, 4, 5 }, "123" };
yield return new object[] { null, "{0}{1}{2}", new object[] { 1, 2, 3, 4, 5, 6 }, "123" };
yield return new object[] { null, "{0}{1}{2}{3}", new object[] { 1, 2, 3, 4, 5 }, "1234" };
yield return new object[] { null, "{0}{1}{2}{3}", new object[] { 1, 2, 3, 4, 5, 6 }, "1234" };
yield return new object[] { null, "{0}{1}{2}{3}", new object[] { 1, 2, 3, 4, 5, 6, 7 }, "1234" };
yield return new object[] { null, "{0}{1}{2}{3}{4}", new object[] { 1, 2, 3, 4, 5, 6 }, "12345" };
yield return new object[] { null, "{0}{1}{2}{3}{4}", new object[] { 1, 2, 3, 4, 5, 6, 7 }, "12345" };
yield return new object[] { null, "{0}{1}{2}{3}{4}", new object[] { 1, 2, 3, 4, 5, 6, 7, 8 }, "12345" };

// Out of order
yield return new object[] { null, "{1}{0}", new object[] { 1, 2 }, "21" };
yield return new object[] { null, "{2}{1}{0}", new object[] { 1, 2, 3 }, "321" };
yield return new object[] { null, "{3}{2}{1}{0}", new object[] { 1, 2, 3, 4 }, "4321" };
yield return new object[] { null, "{4}{3}{2}{1}{0}", new object[] { 1, 2, 3, 4, 5 }, "54321" };

// Longer inputs
yield return new object[] { null, "0 = {0} 1 = {1} 2 = {2} 3 = {3} 4 = {4}", new object[] { "zero", "one", "two", "three", "four" }, "0 = zero 1 = one 2 = two 3 = three 4 = four" };
yield return new object[] { new TestFormatter(), "0 = {0} 1 = {1} 2 = {2} 3 = {3} 4 = {4}", new object[] { "zero", "one", "two", "three", "four" }, "0 = Test: : zero 1 = Test: : one 2 = Test: : two 3 = Test: : three 4 = Test: : four" };
Expand Down Expand Up @@ -2737,7 +2759,7 @@ public static void Format_Valid(IFormatProvider provider, string format, object[
}

[Fact]
public static void Format_Invalid()
public static void Format_Invalid_ArgumentException()
{
var formatter = new TestFormatter();
var obj1 = new object();
Expand All @@ -2751,66 +2773,107 @@ public static void Format_Invalid()
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(null, obj1, obj2, obj3));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(null, obj1, obj2, obj3, obj4));

AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, null, obj1));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, null, obj1, obj2));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, null, obj1, obj2, obj3));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, (string)null, obj1));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, (string)null, obj1, obj2));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, (string)null, obj1, obj2, obj3));

// Args is null
AssertExtensions.Throws<ArgumentNullException>("args", () => string.Format("", null));
AssertExtensions.Throws<ArgumentNullException>("args", () => string.Format(formatter, "", null));

// Args and format are null
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(null, (object[])null));
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, null, null));

Assert.Throws<FormatException>(() => string.Format("{-1}", obj1)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format("{-1}", obj1, obj2)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format("{-1}", obj1, obj2, obj3)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format("{-1}", obj1, obj2, obj3, obj4)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format(formatter, "{-1}", obj1)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format(formatter, "{-1}", obj1, obj2)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format(formatter, "{-1}", obj1, obj2, obj3)); // Format has value < 0
Assert.Throws<FormatException>(() => string.Format(formatter, "{-1}", obj1, obj2, obj3, obj4)); // Format has value < 0

Assert.Throws<FormatException>(() => string.Format("{1}", obj1)); // Format has value >= 1
Assert.Throws<FormatException>(() => string.Format("{2}", obj1, obj2)); // Format has value >= 2
Assert.Throws<FormatException>(() => string.Format("{3}", obj1, obj2, obj3)); // Format has value >= 3
Assert.Throws<FormatException>(() => string.Format("{4}", obj1, obj2, obj3, obj4)); // Format has value >= 4
Assert.Throws<FormatException>(() => string.Format(formatter, "{1}", obj1)); // Format has value >= 1
Assert.Throws<FormatException>(() => string.Format(formatter, "{2}", obj1, obj2)); // Format has value >= 2
Assert.Throws<FormatException>(() => string.Format(formatter, "{3}", obj1, obj2, obj3)); // Format has value >= 3
Assert.Throws<FormatException>(() => string.Format(formatter, "{4}", obj1, obj2, obj3, obj4)); // Format has value >= 4

Assert.Throws<FormatException>(() => string.Format("{", "")); // Format has unescaped {
Assert.Throws<FormatException>(() => string.Format("{a", "")); // Format has unescaped {

Assert.Throws<FormatException>(() => string.Format("}", "")); // Format has unescaped }
Assert.Throws<FormatException>(() => string.Format("}a", "")); // Format has unescaped }
Assert.Throws<FormatException>(() => string.Format("{0:}}", "")); // Format has unescaped }

Assert.Throws<FormatException>(() => string.Format("{\0", "")); // Format has invalid character after {
Assert.Throws<FormatException>(() => string.Format("{a", "")); // Format has invalid character after {

Assert.Throws<FormatException>(() => string.Format("{0 ", "")); // Format with index and spaces is not closed

Assert.Throws<FormatException>(() => string.Format("{1000000", new string[10])); // Format index is too long
Assert.Throws<FormatException>(() => string.Format("{10000000}", new string[10])); // Format index is too long
AssertExtensions.Throws<ArgumentNullException>("format", () => string.Format(formatter, (string)null, null));
}

Assert.Throws<FormatException>(() => string.Format("{0,", "")); // Format with comma is not closed
Assert.Throws<FormatException>(() => string.Format("{0, ", "")); // Format with comma and spaces is not closed
Assert.Throws<FormatException>(() => string.Format("{0,-", "")); // Format with comma and minus sign is not closed
public static IEnumerable<object[]> Format_Invalid_FormatExceptionFromFormat_MemberData()
{
var obj1 = new object();
var obj2 = new object();
var obj3 = new object();
var obj4 = new object();

Assert.Throws<FormatException>(() => string.Format("{0,-\0", "")); // Format has invalid character after minus sign
Assert.Throws<FormatException>(() => string.Format("{0,-a", "")); // Format has invalid character after minus sign
foreach (IFormatProvider provider in new[] { null, new TestFormatter() })
{
yield return new object[] { provider, "{-1}", new object[] { obj1 } }; // Format has value < 0
yield return new object[] { provider, "{-1}", new object[] { obj1, obj2 } }; // Format has value < 0
yield return new object[] { provider, "{-1}", new object[] { obj1, obj2, obj3 } }; // Format has value < 0
yield return new object[] { provider, "{-1}", new object[] { obj1, obj2, obj3, obj4 } }; // Format has value < 0
yield return new object[] { provider, "{", new object[] { "" } }; // Format has unescaped {
yield return new object[] { provider, "{a", new object[] { "" } }; // Format has unescaped {
yield return new object[] { provider, "}", new object[] { "" } }; // Format has unescaped }
yield return new object[] { provider, "}a", new object[] { "" } }; // Format has unescaped }
yield return new object[] { provider, "{0:}}", new object[] { "" } }; // Format has unescaped }
yield return new object[] { provider, "{\0", new object[] { "" } }; // Format has invalid character after {
yield return new object[] { provider, "{a", new object[] { "" } }; // Format has invalid character after {
yield return new object[] { provider, "{0 ", new object[] { "" } }; // Format with index and spaces is not closed
yield return new object[] { provider, "{1000000", new object[10] }; // Format has missing closing brace
yield return new object[] { provider, "{0,", new object[] { "" } }; // Format with comma is not closed
yield return new object[] { provider, "{0, ", new object[] { "" } }; // Format with comma and spaces is not closed
yield return new object[] { provider, "{0,-", new object[] { "" } }; // Format with comma and minus sign is not closed
yield return new object[] { provider, "{0,-\0", new object[] { "" } }; // Format has invalid character after minus sign
yield return new object[] { provider, "{0,-a", new object[] { "" } }; // Format has invalid character after minus sign
yield return new object[] { provider, "{0,1000000", new string[10] }; // Format length is too long
yield return new object[] { provider, "{0:", new string[10] }; // Format with colon is not closed
yield return new object[] { provider, "{0: ", new string[10] }; // Format with colon and spaces is not closed
yield return new object[] { provider, "{0:{", new string[10] }; // Format with custom format contains unescaped {
yield return new object[] { provider, "{0:{}", new string[10] }; // Format with custom format contains unescaped {
}
}

public static IEnumerable<object[]> Format_Invalid_FormatExceptionFromArgs_MemberData()
{
var obj1 = new object();
var obj2 = new object();
var obj3 = new object();
var obj4 = new object();

Assert.Throws<FormatException>(() => string.Format("{0,1000000", new string[10])); // Format length is too long
Assert.Throws<FormatException>(() => string.Format("{0,10000000}", new string[10])); // Format length is too long
foreach (IFormatProvider provider in new[] { null, new TestFormatter() })
{
yield return new object[] { provider, "{1}", new object[] { obj1 } }; // Format has value >= 1
yield return new object[] { provider, "{2}", new object[] { obj1, obj2 } }; // Format has value >= 2
yield return new object[] { provider, "{3}", new object[] { obj1, obj2, obj3 } }; // Format has value >= 3
yield return new object[] { provider, "{4}", new object[] { obj1, obj2, obj3, obj4 } }; // Format has value >= 4
}
}

Assert.Throws<FormatException>(() => string.Format("{0:", new string[10])); // Format with colon is not closed
Assert.Throws<FormatException>(() => string.Format("{0: ", new string[10])); // Format with colon and spaces is not closed
[Theory]
[MemberData(nameof(Format_Invalid_FormatExceptionFromFormat_MemberData))]
[MemberData(nameof(Format_Invalid_FormatExceptionFromArgs_MemberData))]
[InlineData(null, "{10000000}", new object[] { null })]
[InlineData(null, "{0,10000000}", new string[] { null })]
public static void Format_Invalid_FormatExceptionFromFormatOrArgs(IFormatProvider provider, string format, object[] args)
{
if (provider is null)
{
Assert.Throws<FormatException>(() => string.Format(format, args));
switch (args.Length)
{
case 1:
Assert.Throws<FormatException>(() => string.Format(format, args[0]));
break;
case 2:
Assert.Throws<FormatException>(() => string.Format(format, args[0], args[1]));
break;
case 3:
Assert.Throws<FormatException>(() => string.Format(format, args[0], args[1], args[2]));
break;
}
}

Assert.Throws<FormatException>(() => string.Format("{0:{", new string[10])); // Format with custom format contains unescaped {
Assert.Throws<FormatException>(() => string.Format("{0:{}", new string[10])); // Format with custom format contains unescaped {
Assert.Throws<FormatException>(() => string.Format(provider, format, args));
switch (args.Length)
{
case 1:
Assert.Throws<FormatException>(() => string.Format(provider, format, args[0]));
break;
case 2:
Assert.Throws<FormatException>(() => string.Format(provider, format, args[0], args[1]));
break;
case 3:
Assert.Throws<FormatException>(() => string.Format(provider, format, args[0], args[1], args[2]));
break;
}
}

[ConditionalTheory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ internal bool TryGetMemberMethodExplicit(string name, out ComMethodDesc method)
return false;
}

throw Error.CouldNotGetDispId(name, string.Format(CultureInfo.InvariantCulture, "0x{0:X})", hresult));
throw Error.CouldNotGetDispId(name, $"0x{(uint)hresult:X})");
}

[RequiresUnreferencedCode(Binder.TrimmerWarning)]
Expand Down Expand Up @@ -267,7 +267,7 @@ internal bool TryGetPropertySetterExplicit(string name, out ComMethodDesc method
return false;
}

throw Error.CouldNotGetDispId(name, string.Format(CultureInfo.InvariantCulture, "0x{0:X})", hresult));
throw Error.CouldNotGetDispId(name, $"0x{(uint)hresult:X})");
}

[RequiresUnreferencedCode(Binder.TrimmerWarning)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ public void StartProperty(string name)
{
_firstItem = false;
}
Builder.AppendFormat("\"{0}\":", name);

Builder.Append('"').Append(name).Append("\":");
}

public void StartArrayItem()
Expand All @@ -179,7 +180,7 @@ public void WriteProperty(string name, object? value)
StartProperty(name);
if (value != null)
{
Builder.AppendFormat(" \"{0}\"", value);
Builder.Append(" \"").Append(value).Append('"');
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ private void AppendResolutionPath(StringBuilder builder, Type currentlyResolving
}
else
{
builder.AppendFormat("{0}({1})",
TypeNameHelper.GetTypeDisplayName(serviceType),
TypeNameHelper.GetTypeDisplayName(implementationType));
builder.Append(TypeNameHelper.GetTypeDisplayName(serviceType))
.Append('(')
.Append(TypeNameHelper.GetTypeDisplayName(implementationType))
.Append(')');
}

builder.Append(" -> ");
Expand Down

0 comments on commit 87b9376

Please sign in to comment.