diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index d2c023ea00..1120d2c753 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -699,7 +699,28 @@ private static void GenerateAssertConditionClassForMethod(SourceProductionContex sourceBuilder.AppendLine(); } - if (!attributeData.TargetType.IsValueType) + // Check if the method's first parameter accepts null values + // For static methods like string.IsNullOrEmpty(string? value), the first parameter + // is the value being asserted. If it's marked as nullable (NullableAnnotation.Annotated), + // we should skip the null check and let the method handle null values. + var shouldGenerateNullCheck = !attributeData.TargetType.IsValueType; + if (shouldGenerateNullCheck && staticMethod.Parameters.Length > 0) + { + var firstParameter = staticMethod.Parameters[0]; + // Skip null check if the parameter explicitly accepts null (e.g., string? value) + if (firstParameter.NullableAnnotation == NullableAnnotation.Annotated) + { + shouldGenerateNullCheck = false; + } + // For backwards compatibility with .NET Framework where NullableAnnotation might not be set, + // also check for well-known methods that accept null by design + else if (methodName == "IsNullOrEmpty" || methodName == "IsNullOrWhiteSpace") + { + shouldGenerateNullCheck = false; + } + } + + if (shouldGenerateNullCheck) { sourceBuilder.AppendLine(" if (actualValue is null)"); sourceBuilder.AppendLine(" {"); diff --git a/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs b/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs new file mode 100644 index 0000000000..45c7b48969 --- /dev/null +++ b/TUnit.Assertions.Tests/StringNullabilityAssertionTests.cs @@ -0,0 +1,91 @@ +using TUnit.Assertions.Extensions; + +namespace TUnit.Assertions.Tests; + +/// +/// Tests for string assertions that accept null values (IsNullOrEmpty, IsNullOrWhiteSpace) +/// +public class StringNullabilityAssertionTests +{ + [Test] + public async Task IsNullOrEmpty_WithNullString_Passes() + { + string? nullString = null; + await Assert.That(nullString).IsNullOrEmpty(); + } + + [Test] + public async Task IsNullOrEmpty_WithEmptyString_Passes() + { + var emptyString = ""; + await Assert.That(emptyString).IsNullOrEmpty(); + } + + [Test] + public async Task IsNullOrEmpty_WithNonEmptyString_Fails() + { + var value = "Hello"; + await Assert.That(async () => await Assert.That(value).IsNullOrEmpty()) + .Throws(); + } + + [Test] + public async Task IsNullOrEmpty_WithWhitespace_Fails() + { + var value = " "; + await Assert.That(async () => await Assert.That(value).IsNullOrEmpty()) + .Throws(); + } + + [Test] + public async Task IsNullOrWhiteSpace_WithNullString_Passes() + { + string? nullString = null; + await Assert.That(nullString).IsNullOrWhiteSpace(); + } + + [Test] + public async Task IsNullOrWhiteSpace_WithEmptyString_Passes() + { + var emptyString = ""; + await Assert.That(emptyString).IsNullOrWhiteSpace(); + } + + [Test] + public async Task IsNullOrWhiteSpace_WithWhitespace_Passes() + { + var whitespace = " "; + await Assert.That(whitespace).IsNullOrWhiteSpace(); + } + + [Test] + public async Task IsNullOrWhiteSpace_WithNonEmptyString_Fails() + { + var value = "Hello"; + await Assert.That(async () => await Assert.That(value).IsNullOrWhiteSpace()) + .Throws(); + } + + [Test] + public async Task IsNotNullOrEmpty_WithNullString_Fails() + { + string? nullString = null; + await Assert.That(async () => await Assert.That(nullString).IsNotNullOrEmpty()) + .Throws(); + } + + [Test] + public async Task IsNotNullOrEmpty_WithEmptyString_Fails() + { + var emptyString = ""; + await Assert.That(async () => await Assert.That(emptyString).IsNotNullOrEmpty()) + .Throws(); + } + + [Test] + public async Task IsNotNullOrEmpty_WithNonEmptyString_Passes() + { + var value = "Hello"; + await Assert.That(value).IsNotNullOrEmpty(); + } +}