Skip to content

Commit

Permalink
Merge pull request #714 from SteveDunn/infinite-loop-in-tostring
Browse files Browse the repository at this point in the history
Fix: Infinite loop in ToString when used in a formatted string
  • Loading branch information
SteveDunn authored Nov 22, 2024
2 parents 8c4b320 + ce066a9 commit 954c14a
Show file tree
Hide file tree
Showing 7,453 changed files with 15,751 additions and 15,112 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
4 changes: 3 additions & 1 deletion Consumers.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="TEST_TYPE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb_aaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CustomTools/CustomToolsData/@EntryValue"></s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vogen/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=historicweatherforecast/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vogen/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=weatherforecast/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
1 change: 0 additions & 1 deletion samples/OrleansExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Orleans.Runtime;
using OrleansExample;

var builder = WebApplication.CreateBuilder(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using MessagePack;
using MessagePack.Formatters;

namespace Vogen.Examples.SerializationAndConversion.MessagePackScenario.UsingConversionAttributes;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using MessagePack;
using MessagePack.Formatters;

namespace Vogen.Examples.SerializationAndConversion.MessagePackScenario.UsingMarkers;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#endif

using System;
using System.Linq;
using System.Threading.Tasks;
using Bogus;
using JetBrains.Annotations;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
Expand Down
1 change: 0 additions & 1 deletion samples/WebApplication/OrdersController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Vogen;

[Route("api/[controller]")]
public class OrdersController : ControllerBase
Expand Down
4 changes: 0 additions & 4 deletions samples/WebApplication/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Vogen;

#if USE_SWASHBUCKLE
using Swashbuckle.AspNetCore.SwaggerGen;
#endif
#if USE_MICROSOFT_OPENAPI_AND_SCALAR
using Scalar.AspNetCore;
Expand Down
2 changes: 1 addition & 1 deletion samples/WebApplication/__ProduceDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Vogen;


// If Vogen find this type in this namespace, it'll generate a `diganostics.cs`, which is just a
// C# file with a multi-line comment describing what language version, targets, config etc.
Expand Down
9 changes: 6 additions & 3 deletions src/Vogen/GenerateCodeForTryFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private static void BuildFor(StringBuilder sb,
continue;
}

sb.AppendLine(Hoisting.HoistMethodFromPrimitive(
string hoistMethodFromPrimitive = Hoisting.HoistMethodFromPrimitive(
primitiveMethod,
interfaceSymbol!,
(valueAccessor, parameterNames) =>
Expand All @@ -80,13 +80,16 @@ private static void BuildFor(StringBuilder sb,
return $"""return IsInitialized() ? {valueAccessor}.ToString({parameterNames}) : "[UNINITIALIZED]";""";
}


if (formattableType is FormattableType.TryFormat)
{
return $"return IsInitialized() ? {valueAccessor}.TryFormat({parameterNames}) : false;";
return $"return IsInitialized() ? {valueAccessor}.TryFormat({parameterNames}) : true;";
}

return "return default!";
}));
});

sb.AppendLine(hoistMethodFromPrimitive);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
#pragma warning disable CS8602 // Dereference of a possibly null reference.

namespace ConsumerTests.ParseAndTryParseTests;
namespace ConsumerTests.ParsingAndFormattingTests;

public class Tests
{
Expand Down
60 changes: 60 additions & 0 deletions tests/ConsumerTests/ParsingAndFormattingTests/TryFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Globalization;
using Vogen.Tests.Types;

namespace ConsumerTests.ParsingAndFormattingTests;

#pragma warning disable CS8625, CS8602
public class TryFormatTests
{
[Fact]
public void TryFormat_delegates_to_primitive()
{
Span<char> s = stackalloc char[8];
Age.From(128).TryFormat(s, out int written, "x8", CultureInfo.InvariantCulture).Should().BeTrue();
written.Should().Be(8);
s.ToString().Should().Be("00000080");

MyDecimal d = MyDecimal.From(1.23m);

$"{d:0.000}".Should().Be("1.230");
d.ToString("0.00", new CultureInfo("fr")).Should().Be("1,23");
$"{d:0.000}".Should().Be("1.230");

Span<char> s2 = stackalloc char[8];
MyDecimal.From(1.23m).TryFormat(s2, out written, "000.00", CultureInfo.InvariantCulture).Should().BeTrue();
written.Should().Be(6);
s2[..written].ToString().Should().Be("001.23");
}

/// <summary>
/// See https://github.com/SteveDunn/Vogen/issues/710
/// An uninitialized VO, when used in a formatted string, used to hang because it returned
/// false from `TryFormat`. False is only ever used in the provided span wasn't big enough, which
/// caused the runtime to increase the size and retry.
/// </summary>
[SkippableIfBuiltWithNoValidationFlagFact]
public void Uninitialized_vos_output_nothing()
{
var vo = new TestContainer();
vo.ToString().Should().Be("ID1:'' - ID2:'00000000-0000-0000-0000-000000000000'");
}

[SkippableIfNotBuiltWithNoValidationFlagFact]
public void Uninitialized_vos_output_default_value_when_validation_is_turned_off()
{
var vo = new TestContainer();
vo.ToString().Should().Be("ID1:'00000000-0000-0000-0000-000000000000' - ID2:'00000000-0000-0000-0000-000000000000'");
}

public class TestContainer
{
public MyId Id1 { get; set; }
public MyId Id2 { get; set; } = MyId.From(Guid.Empty);

public override string ToString()
=> $"ID1:'{Id1}' - ID2:'{Id2}'";
}
}

[ValueObject<Guid>]
public readonly partial struct MyId;
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using System.Linq;
// ReSharper disable RedundantRecordClassKeyword
// ReSharper disable ArrangeStaticMemberQualifier

namespace ConsumerTests.ParsingAndFormattingTests;

[ValueObject<decimal>]
public partial struct MyDecimal;

namespace ConsumerTests.ParseAndTryParseTests;

[ValueObject(typeof(int), parsableForPrimitives: ParsableForPrimitives.GenerateNothing)]
public partial struct VoNoParsableNoHoisting
{
}
public partial struct VoNoParsableNoHoisting;

[ValueObject(typeof(int))]
public partial struct StructIntVoNoValidation
{
}
public partial struct StructIntVoNoValidation;

[ValueObject(typeof(string), parsableForStrings: ParsableForStrings.GenerateMethodsAndInterface )]
public partial struct VoWithOwnInstanceParseMethod
Expand Down Expand Up @@ -50,19 +52,13 @@ public static VoIntWithOwnStaticParseMethodWithFormatProvider Parse(string s, IF
}

[ValueObject(typeof(int))]
public partial class ClassIntVoNoValidation
{
}
public partial class ClassIntVoNoValidation;

[ValueObject(typeof(int))]
public partial class RecordClassIntVoNoValidation
{
}
public partial class RecordClassIntVoNoValidation;

[ValueObject(typeof(int))]
public partial class RecordStructIntVoNoValidation
{
}
public partial class RecordStructIntVoNoValidation;

[ValueObject<int>]
public partial struct StructIntVo
Expand Down Expand Up @@ -102,28 +98,28 @@ private static Validation Validate(int input) =>
}

[ValueObject(typeof(byte))]
public partial struct ByteVo { }
public partial struct ByteVo;

[ValueObject(typeof(bool))]
public partial struct StructBoolVo { }
public partial struct StructBoolVo;

[ValueObject(typeof(bool))]
public partial struct RecordStructBoolVo { }
public partial struct RecordStructBoolVo;

[ValueObject(typeof(bool))]
public partial class ClassBoolVo { }
public partial class ClassBoolVo;

[ValueObject(typeof(bool))]
public partial record class RecordClassBoolVo { }
public partial record class RecordClassBoolVo;

[ValueObject(typeof(char))]
public partial struct CharVo { }
public partial struct CharVo;

[ValueObject(typeof(decimal))]
public partial struct DecimalVo { }
public partial struct DecimalVo;

[ValueObject(typeof(double))]
public partial struct DoubleVo { }
public partial struct DoubleVo;

public class C : IParsable<C>
{
Expand All @@ -133,4 +129,4 @@ public class C : IParsable<C>
}

[ValueObject<C>]
public partial struct MyCustomVo { }
public partial struct MyCustomVo;
24 changes: 2 additions & 22 deletions tests/ConsumerTests/ToStringTests/BasicFunctionality.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Globalization;
using Vogen.Tests.Types;
using Vogen.Tests.Types;

namespace ConsumerTests.ToStringTests;

Expand Down Expand Up @@ -81,28 +80,9 @@ public void ToString_with_format_uses_IFormattable_methods()
Name.From("barney").ToString().Should().Be("barney");
Name.From("wilma").ToString().Should().Be("wilma");
}

[Fact]
public void TryFormat_delegates_to_primitive()
{
Span<char> s = stackalloc char[8];
Age.From(128).TryFormat(s, out int written, "x8", CultureInfo.InvariantCulture).Should().BeTrue();
written.Should().Be(8);
s.ToString().Should().Be("00000080");

MyDecimal d = MyDecimal.From(1.23m);

$"{d:0.000}".Should().Be("1.230");
d.ToString("0.00", new CultureInfo("fr")).Should().Be("1,23");
$"{d:0.000}".Should().Be("1.230");

Span<char> s2 = stackalloc char[8];
MyDecimal.From(1.23m).TryFormat(s2, out written, "000.00", CultureInfo.InvariantCulture).Should().BeTrue();
written.Should().Be(6);
s2[..written].ToString().Should().Be("001.23");
}
}


[ValueObject<NaughtyPrimitive>]
public partial class VoWrappingNaughtyPrimitive
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,14 +905,14 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!;
public bool TryFormat(global::System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
charsWritten = default;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : true;
}

/// <inheritdoc cref = "int.TryFormat(System.Span{byte}, out int, System.ReadOnlySpan{char}, System.IFormatProvider? )"/>
public bool TryFormat(global::System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
bytesWritten = default;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : true;
}

#nullable restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,14 +632,14 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!;
public bool TryFormat(global::System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
charsWritten = default;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : true;
}

/// <inheritdoc cref = "int.TryFormat(System.Span{byte}, out int, System.ReadOnlySpan{char}, System.IFormatProvider? )"/>
public bool TryFormat(global::System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
bytesWritten = default;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : true;
}

#nullable restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,14 +528,14 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!;
public bool TryFormat(global::System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
charsWritten = default;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : true;
}

/// <inheritdoc cref = "int.TryFormat(System.Span{byte}, out int, System.ReadOnlySpan{char}, System.IFormatProvider? )"/>
public bool TryFormat(global::System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
bytesWritten = default;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : true;
}

#nullable restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,14 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!;
bool System.ISpanFormattable.TryFormat(global::System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
charsWritten = default;
return IsInitialized() ? (Value as System.ISpanFormattable).TryFormat(destination, out charsWritten, format, provider) : false;
return IsInitialized() ? (Value as System.ISpanFormattable).TryFormat(destination, out charsWritten, format, provider) : true;
}

/// <inheritdoc cref = "System.IUtf8SpanFormattable"/>
bool System.IUtf8SpanFormattable.TryFormat(global::System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
bytesWritten = default;
return IsInitialized() ? (Value as System.IUtf8SpanFormattable).TryFormat(utf8Destination, out bytesWritten, format, provider) : false;
return IsInitialized() ? (Value as System.IUtf8SpanFormattable).TryFormat(utf8Destination, out bytesWritten, format, provider) : true;
}

#nullable restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,14 +528,14 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!;
public bool TryFormat(global::System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
charsWritten = default;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : true;
}

/// <inheritdoc cref = "int.TryFormat(System.Span{byte}, out int, System.ReadOnlySpan{char}, System.IFormatProvider? )"/>
public bool TryFormat(global::System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
bytesWritten = default;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : true;
}

#nullable restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,14 +527,14 @@ private readonly global::System.Diagnostics.StackTrace _stackTrace = null!;
public bool TryFormat(global::System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
charsWritten = default;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(destination, out charsWritten, format, provider) : true;
}

/// <inheritdoc cref = "int.TryFormat(System.Span{byte}, out int, System.ReadOnlySpan{char}, System.IFormatProvider? )"/>
public bool TryFormat(global::System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] global::System.ReadOnlySpan<char> format, global::System.IFormatProvider provider)
{
bytesWritten = default;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : false;
return IsInitialized() ? Value.TryFormat(utf8Destination, out bytesWritten, format, provider) : true;
}

#nullable restore
Expand Down
Loading

0 comments on commit 954c14a

Please sign in to comment.