From 40b1b848a02b901b3dd3ec75c8311893f32a4ce4 Mon Sep 17 00:00:00 2001 From: ancplua Date: Thu, 7 May 2026 02:55:19 +0200 Subject: [PATCH] feat(utilities): add TryResult for bool TryX(out T) authoring Add author-side TryResult helpers that initialize out parameters while returning the surrounding TryX result value. Cover bool failure and success paths for one, two, and three out parameters, plus non-bool sentinel failure returns. Add focused tests for defaulting, value assignment, non-nullable value-type out parameters, and composition inside a try-parse method. --- src/ANcpLua.Roslyn.Utilities/TryResult.cs | 274 ++++++++++++++++++ .../TryResultTests.cs | 93 ++++++ 2 files changed, 367 insertions(+) create mode 100644 src/ANcpLua.Roslyn.Utilities/TryResult.cs create mode 100644 tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TryResultTests.cs diff --git a/src/ANcpLua.Roslyn.Utilities/TryResult.cs b/src/ANcpLua.Roslyn.Utilities/TryResult.cs new file mode 100644 index 0000000..87aad95 --- /dev/null +++ b/src/ANcpLua.Roslyn.Utilities/TryResult.cs @@ -0,0 +1,274 @@ +using System.Runtime.CompilerServices; + +namespace ANcpLua.Roslyn.Utilities; + +/// +/// Author-side helpers for bool TryX(out T) patterns: compact one-liners +/// that initialize out parameters on the failure or success return path. +/// +/// +/// +/// Pairs with , the consumer-side surface that turns +/// bool + out into T?. is the opposite direction: +/// it exists for code that writes a bool TryFoo(out T) method and wants to +/// collapse the failure or success branch from three or four lines to one. +/// +/// +/// All overloads request , keeping them close to a hand-written +/// result = default; return false; on the failure path or +/// result = value; return true; on the success path. +/// +/// +/// On .NET 9+, the type parameters carry allows ref struct, so the helpers also work when +/// an out parameter is a , , +/// or any other ref struct — something the BCL's Try* shape can't express generically. +/// On netstandard2.0 the relaxation is conditionally compiled out. +/// +/// +/// The family generalizes past bool-returning +/// try-methods: use it when the enclosing method returns an enum status, an int error code, or any +/// non-bool sentinel. +/// +/// +/// +/// +/// // Multi-out failure path: from four lines to one. +/// public bool TryParse(string input, out Foo foo, out Bar bar) +/// { +/// if (!IsValid(input)) +/// return TryResult.Fail(out foo, out bar); +/// +/// foo = ParseFoo(input); +/// bar = ParseBar(input); +/// return true; +/// } +/// +/// // Single-out success and failure on one expression each. +/// public bool TryGetCached(string key, out Value result) +/// => _cache.TryGetValue(key, out var hit) +/// ? TryResult.Ok(hit, out result) +/// : TryResult.Fail(out result); +/// +/// // Non-bool return type. +/// public ParseStatus TryParse(string input, out Foo foo) +/// { +/// if (string.IsNullOrEmpty(input)) +/// return TryResult.Fail(ParseStatus.EmptyInput, out foo); +/// // ... +/// } +/// +/// +/// +#if ANCPLUA_ROSLYN_PUBLIC +public +#else +internal +#endif + static class TryResult +{ + /// + /// Sets to default and returns false. + /// + /// The type of the out parameter. + /// When this method returns, contains default(T). + /// false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Fail(out T? result) +#if NET9_0_OR_GREATER + where T : allows ref struct +#endif + { + result = default; + return false; + } + + /// + /// Sets two out parameters to default and returns false. + /// + /// The type of the first out parameter. + /// The type of the second out parameter. + /// When this method returns, contains default(T1). + /// When this method returns, contains default(T2). + /// false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Fail(out T1? result1, out T2? result2) +#if NET9_0_OR_GREATER + where T1 : allows ref struct + where T2 : allows ref struct +#endif + { + result1 = default; + result2 = default; + return false; + } + + /// + /// Sets three out parameters to default and returns false. + /// + /// The type of the first out parameter. + /// The type of the second out parameter. + /// The type of the third out parameter. + /// When this method returns, contains default(T1). + /// When this method returns, contains default(T2). + /// When this method returns, contains default(T3). + /// false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Fail(out T1? result1, out T2? result2, out T3? result3) +#if NET9_0_OR_GREATER + where T1 : allows ref struct + where T2 : allows ref struct + where T3 : allows ref struct +#endif + { + result1 = default; + result2 = default; + result3 = default; + return false; + } + + /// + /// Sets to default and returns . + /// + /// + /// Use when the enclosing TryX method returns something other than bool + /// (e.g. an enum status code, a parsed primary value, or a non-trivial sentinel). + /// + /// The return type of the enclosing method. + /// The type of the out parameter. + /// The value to return. + /// When this method returns, contains default(T). + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TReturn Fail(TReturn returnValue, out T? result) +#if NET9_0_OR_GREATER + where TReturn : allows ref struct + where T : allows ref struct +#endif + { + result = default; + return returnValue; + } + + /// + /// Sets two out parameters to default and returns . + /// + /// The return type of the enclosing method. + /// The type of the first out parameter. + /// The type of the second out parameter. + /// The value to return. + /// When this method returns, contains default(T1). + /// When this method returns, contains default(T2). + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TReturn Fail(TReturn returnValue, out T1? result1, out T2? result2) +#if NET9_0_OR_GREATER + where TReturn : allows ref struct + where T1 : allows ref struct + where T2 : allows ref struct +#endif + { + result1 = default; + result2 = default; + return returnValue; + } + + /// + /// Sets three out parameters to default and returns . + /// + /// The return type of the enclosing method. + /// The type of the first out parameter. + /// The type of the second out parameter. + /// The type of the third out parameter. + /// The value to return. + /// When this method returns, contains default(T1). + /// When this method returns, contains default(T2). + /// When this method returns, contains default(T3). + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TReturn Fail(TReturn returnValue, + out T1? result1, out T2? result2, out T3? result3) +#if NET9_0_OR_GREATER + where TReturn : allows ref struct + where T1 : allows ref struct + where T2 : allows ref struct + where T3 : allows ref struct +#endif + { + result1 = default; + result2 = default; + result3 = default; + return returnValue; + } + + /// + /// Assigns to and returns true. + /// + /// + /// The success-path counterpart to . Lets the success and failure + /// branches of a TryX method be expressed as conditional expressions rather than + /// if/else blocks. + /// + /// The type of the out parameter. + /// The value to assign. + /// When this method returns, contains . + /// true. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Ok(T value, out T result) +#if NET9_0_OR_GREATER + where T : allows ref struct +#endif + { + result = value; + return true; + } + + /// + /// Assigns the supplied values to two out parameters and returns true. + /// + /// The type of the first out parameter. + /// The type of the second out parameter. + /// The value assigned to . + /// The value assigned to . + /// When this method returns, contains . + /// When this method returns, contains . + /// true. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Ok(T1 value1, T2 value2, out T1 result1, out T2 result2) +#if NET9_0_OR_GREATER + where T1 : allows ref struct + where T2 : allows ref struct +#endif + { + result1 = value1; + result2 = value2; + return true; + } + + /// + /// Assigns the supplied values to three out parameters and returns true. + /// + /// The type of the first out parameter. + /// The type of the second out parameter. + /// The type of the third out parameter. + /// The value assigned to . + /// The value assigned to . + /// The value assigned to . + /// When this method returns, contains . + /// When this method returns, contains . + /// When this method returns, contains . + /// true. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Ok(T1 value1, T2 value2, T3 value3, + out T1 result1, out T2 result2, out T3 result3) +#if NET9_0_OR_GREATER + where T1 : allows ref struct + where T2 : allows ref struct + where T3 : allows ref struct +#endif + { + result1 = value1; + result2 = value2; + result3 = value3; + return true; + } +} diff --git a/tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TryResultTests.cs b/tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TryResultTests.cs new file mode 100644 index 0000000..5e4ce72 --- /dev/null +++ b/tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TryResultTests.cs @@ -0,0 +1,93 @@ +using ANcpLua.Roslyn.Utilities; +using AwesomeAssertions; +using Xunit; + +namespace ANcpLua.Roslyn.Utilities.Testing.Tests; + +public sealed class TryResultTests +{ + [Fact] + public void Fail_DefaultsAllOutsAndReturnsFalse() + { + TryResult.Fail(out string? a).Should().BeFalse(); + a.Should().BeNull(); + + TryResult.Fail(out int value).Should().BeFalse(); + value.Should().Be(default); + + TryResult.Fail(out string? b1, out int? b2).Should().BeFalse(); + b1.Should().BeNull(); + b2.Should().BeNull(); + + TryResult.Fail(out string? c1, out int? c2, out double? c3).Should().BeFalse(); + c1.Should().BeNull(); + c2.Should().BeNull(); + c3.Should().BeNull(); + } + + [Fact] + public void FailWithReturnValue_PropagatesReturnAndDefaultsOuts() + { + TryResult.Fail(ParseStatus.EmptyInput, out string? a).Should().Be(ParseStatus.EmptyInput); + a.Should().BeNull(); + + TryResult.Fail(-1, out int? b1, out string? b2).Should().Be(-1); + b1.Should().BeNull(); + b2.Should().BeNull(); + + TryResult.Fail("err", out int? c1, out int? c2, out int? c3).Should().Be("err"); + c1.Should().BeNull(); + c2.Should().BeNull(); + c3.Should().BeNull(); + } + + [Fact] + public void Ok_AssignsValuesAndReturnsTrue() + { + TryResult.Ok(42, out int a).Should().BeTrue(); + a.Should().Be(42); + + TryResult.Ok(1, "x", out int b1, out string b2).Should().BeTrue(); + b1.Should().Be(1); + b2.Should().Be("x"); + + TryResult.Ok(1, "x", 3.14, out int c1, out string c2, out double c3).Should().BeTrue(); + c1.Should().Be(1); + c2.Should().Be("x"); + c3.Should().Be(3.14); + } + + [Fact] + public void ComposesIntoReadableTryParse() + { + TryParseTwoInts("1,2", out var a, out var b).Should().BeTrue(); + a.Should().Be(1); + b.Should().Be(2); + + TryParseTwoInts("oops", out a, out b).Should().BeFalse(); + a.Should().BeNull(); + b.Should().BeNull(); + + TryParseTwoInts("1,bad", out a, out b).Should().BeFalse(); + a.Should().BeNull(); + b.Should().BeNull(); + } + + private static bool TryParseTwoInts(string input, out int? first, out int? second) + { + var parts = input.Split(','); + if (parts.Length != 2 + || !int.TryParse(parts[0], out var parsedFirst) + || !int.TryParse(parts[1], out var parsedSecond)) + return TryResult.Fail(out first, out second); + + return TryResult.Ok(parsedFirst, parsedSecond, out first, out second); + } + + private enum ParseStatus + { + Ok, + EmptyInput, + Invalid + } +}