diff --git a/src/AndreGoepel.Core/Result.cs b/src/AndreGoepel.Core/Result.cs index e235bd8..087f1f4 100644 --- a/src/AndreGoepel.Core/Result.cs +++ b/src/AndreGoepel.Core/Result.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace AndreGoepel.Core; /// @@ -6,10 +8,12 @@ namespace AndreGoepel.Core; /// public record Result { + [MemberNotNullWhen(false, nameof(Error))] public bool IsSuccess { get; init; } public string? Error { get; init; } + [MemberNotNullWhen(true, nameof(Error))] public bool IsFailure => !IsSuccess; public static Result Ok() => new() { IsSuccess = true }; diff --git a/tests/AndreGoepel.Core.Tests/Result.Tests.cs b/tests/AndreGoepel.Core.Tests/Result.Tests.cs index 7c0b216..34d1d31 100644 --- a/tests/AndreGoepel.Core.Tests/Result.Tests.cs +++ b/tests/AndreGoepel.Core.Tests/Result.Tests.cs @@ -51,4 +51,42 @@ public void FailOfT_Called_ReturnsFailureWithNoValue() Assert.Equal(default, result.Value); Assert.Equal("invalid input", result.Error); } + + // No null-forgiving operators (!) below and no explicit null checks on Error — + // this only compiles under TreatWarningsAsErrors (CS8602) if the + // [MemberNotNullWhen] attributes on IsSuccess/IsFailure are correct. + + [Fact] + public void IsFailure_True_NarrowsErrorToNonNull() + { + // Arrange + var result = Result.Fail("boom"); + + // Act + var length = result.IsFailure ? result.Error.Length : 0; + + // Assert + Assert.Equal(4, length); + } + + [Fact] + public void IsSuccess_False_NarrowsErrorToNonNullInElseBranch() + { + // Arrange + var result = Result.Fail("boom"); + + // Act + string message; + if (result.IsSuccess) + { + message = "ok"; + } + else + { + message = result.Error; + } + + // Assert + Assert.Equal("boom", message); + } }