Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,18 @@
continue;
}

// Check if the type parameter is a nullable reference type (e.g., string?)
var typeParam = data.AssertionBaseType.TypeArguments[0];
var isNullableReferenceType = typeParam.NullableAnnotation == NullableAnnotation.Annotated &&
typeParam.IsReferenceType;

// Generate positive assertion method
GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false);
GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false, isNullableOverload: false);

// Generate negated assertion method if requested
if (!string.IsNullOrEmpty(data.NegatedMethodName))
{
GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true);
GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true, isNullableOverload: false);
}
}

Expand Down Expand Up @@ -211,7 +216,8 @@
StringBuilder sourceBuilder,
AssertionExtensionData data,
IMethodSymbol constructor,
bool negated)
bool negated,
bool isNullableOverload)
{
var methodName = negated ? data.NegatedMethodName : data.MethodName;
var assertionType = data.ClassSymbol;
Expand Down Expand Up @@ -316,22 +322,41 @@
sourceBuilder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(\"{escapedMessage}\")]");
}

// Add OverloadResolutionPriority attribute only if priority > 0
if (data.OverloadResolutionPriority > 0)
// Add OverloadResolutionPriority attribute if specified
// For nullable overloads (generic with class constraint), increase priority by 1
// so they're preferred over the base nullable overload when source is non-nullable
var effectivePriority = data.OverloadResolutionPriority;
if (isNullableOverload)
{
sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({data.OverloadResolutionPriority})]");
effectivePriority += 1;
}

if (effectivePriority > 0)
{
sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({effectivePriority})]");
}

// Method declaration
var returnType = assertionType.IsGenericType
? $"{assertionType.Name}{genericParamsString}"
: assertionType.Name;

// The extension method always extends IAssertionSource<T> where T is the type argument
// from the Assertion<T> base class. This ensures the source.Context type matches what
// the assertion constructor expects.
// The extension method extends IAssertionSource<T> where T is the type argument
// from the Assertion<T> base class.
string sourceType;
if (typeParam is ITypeParameterSymbol baseTypeParam)
string genericTypeParam = null;

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Konwertowanie literału null lub możliwej wartości null na nienullowalny typ.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Conversion de littéral ayant une valeur null ou d'une éventuelle valeur null en type non-nullable.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Das NULL-Literal oder ein möglicher NULL-Wert wird in einen Non-Nullable-Typ konvertiert.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 347 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.
string genericConstraint = null;

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Konwertowanie literału null lub możliwej wartości null na nienullowalny typ.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Conversion de littéral ayant une valeur null ou d'une éventuelle valeur null en type non-nullable.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Das NULL-Literal oder ein möglicher NULL-Wert wird in einen Non-Nullable-Typ konvertiert.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 348 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.

if (isNullableOverload)
{
// For nullable reference types, we can't use two separate overloads for T and T?
// because NRT annotations are erased at runtime - they're the same type to the CLR.
// Instead, just use the nullable version and accept both nullable and non-nullable sources.
sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>";
genericTypeParam = null;

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Konwertowanie literału null lub możliwej wartości null na nienullowalny typ.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Conversion de littéral ayant une valeur null ou d'une éventuelle valeur null en type non-nullable.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Das NULL-Literal oder ein möglicher NULL-Wert wird in einen Non-Nullable-Typ konvertiert.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 356 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.
genericConstraint = null;

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Konwertowanie literału null lub możliwej wartości null na nienullowalny typ.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Conversion de littéral ayant une valeur null ou d'une éventuelle valeur null en type non-nullable.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Das NULL-Literal oder ein möglicher NULL-Wert wird in einen Non-Nullable-Typ konvertiert.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.

Check warning on line 357 in TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.
}
else if (typeParam is ITypeParameterSymbol baseTypeParam)
{
sourceType = $"IAssertionSource<{baseTypeParam.Name}>";
}
Expand All @@ -340,7 +365,13 @@
sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>";
}

sourceBuilder.Append($" public static {returnType} {methodName}{genericParamsString}(");
sourceBuilder.Append($" public static {returnType} {methodName}");
if (genericTypeParam != null)
{
sourceBuilder.Append($"<{genericTypeParam}>");
}
sourceBuilder.Append(genericParamsString);
sourceBuilder.Append("(");
sourceBuilder.Append($"this {sourceType} source");

// Add additional parameters
Expand All @@ -366,10 +397,16 @@
sourceBuilder.Append(")");

// Add type constraints on new line if any
if (typeConstraints.Count > 0)
var allConstraints = new List<string>(typeConstraints);
if (genericConstraint != null)
{
allConstraints.Add(genericConstraint);
}

if (allConstraints.Count > 0)
{
sourceBuilder.AppendLine();
sourceBuilder.Append($" {string.Join(" ", typeConstraints)}");
sourceBuilder.Append($" {string.Join(" ", allConstraints)}");
}

sourceBuilder.AppendLine();
Expand All @@ -394,7 +431,16 @@
{
sourceBuilder.Append($"<{string.Join(", ", genericParams)}>");
}
sourceBuilder.Append("(source.Context");
sourceBuilder.Append("(");

if (isNullableOverload)
{
sourceBuilder.Append("source.Context.AsNullable()");
}
else
{
sourceBuilder.Append("source.Context");
}

foreach (var param in additionalParams)
{
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ await Assert.That(variable).IsTrue().Because(because)
};

var exception = await Assert.ThrowsAsync<AssertionException>(action);
await Assert.That(exception.Message).IsEqualTo(expectedMessage);
await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage);
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,12 @@ public async Task Conversion_To_Value_Assertion_Builder_On_Casted_Exception_Type
await Assert.That((object)ex).IsAssignableTo<CustomException>();
});

await Assert.That(assertionException).HasMessageStartingWith("""
var expectedPrefix = """
Expected to throw exactly CustomException
and to have message equal to "Foo bar message!"
""");
""";

await Assert.That(assertionException.Message.NormalizeLineEndings()).StartsWith(expectedPrefix.NormalizeLineEndings());
}
}
}
13 changes: 7 additions & 6 deletions TUnit.Assertions.Tests/Bugs/Tests2145.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ public class Tests2145
[Test]
public async Task TestFailMessage()
{
await Assert.That(async () =>
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
{
var val = "hello";

using var _ = Assert.Multiple();
await Assert.That(val).IsEqualTo("world");
await Assert.That(val).IsEqualTo("World");
}).Throws<AssertionException>()
.WithMessage(
"""
});

var expectedMessage = """
Expected to be equal to "world"
but found "hello"

Expand All @@ -24,7 +24,8 @@ Expected to be equal to "World"
but found "hello"

at Assert.That(val).IsEqualTo("World")
"""
);
""";

await Assert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
}
11 changes: 11 additions & 0 deletions TUnit.Assertions.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
global using TUnit.Assertions.Exceptions;
global using TUnit.Assertions.Extensions;
global using TUnit.Core;

internal static class TestHelpers
{
/// <summary>
/// Normalizes all line endings to Unix-style (\n) for consistent cross-platform testing.
/// </summary>
public static string NormalizeLineEndings(this string value)
{
return value.Replace("\r\n", "\n").Replace("\r", "\n");
}
}
135 changes: 135 additions & 0 deletions TUnit.Assertions.Tests/NullabilityWarningTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
namespace TUnit.Assertions.Tests;

/// <summary>
/// Tests to ensure that all combinations of nullable/non-nullable strings
/// work without generating nullability warnings (CS8604, CS8625, etc.)
/// This validates the fix for GitHub issue #3643.
/// </summary>
public class NullabilityWarningTests
{
[Test]
public async Task NonNullableString_WithNonNullableExpected_NoWarning()
{
string actualValue = "test";
string expectedValue = "test";

// Non-nullable to non-nullable should work
await Assert.That(actualValue).IsEqualTo(expectedValue);
}

[Test]
public async Task NonNullableString_WithNullableExpected_NoWarning()
{
string actualValue = "test";
string? expectedValue = "test";

// Non-nullable string should accept nullable expected without warning
await Assert.That(actualValue).IsEqualTo(expectedValue);
}

[Test]
public async Task NullableString_WithNonNullableExpected_NoWarning()
{
string? actualValue = "test";
string expectedValue = "test";

// Nullable string should accept non-nullable expected without warning
await Assert.That(actualValue).IsEqualTo(expectedValue);
}

[Test]
public async Task NullableString_WithNullableExpected_NoWarning()
{
string? actualValue = "test";
string? expectedValue = "test";

// Nullable to nullable should work (this is the main issue from #3643)
await Assert.That(actualValue).IsEqualTo(expectedValue);
}

[Test]
public async Task NonNullableString_WithNullLiteral_NoWarning()
{
string actualValue = "test";

// Passing null literal should be allowed (tests for null mismatch)
await Assert.ThrowsAsync<AssertionException>(async () =>
await Assert.That(actualValue).IsEqualTo(null));
}

[Test]
public async Task NullableString_WithNullLiteral_NoWarning()
{
string? actualValue = null;

// Nullable string with null literal should work
await Assert.That(actualValue).IsEqualTo(null);
}

[Test]
public async Task NonNullableString_WithStringComparison_NoWarning()
{
string actualValue = "TEST";
string? expectedValue = "test";

// Non-nullable with StringComparison parameter and nullable expected
await Assert.That(actualValue).IsEqualTo(expectedValue, StringComparison.OrdinalIgnoreCase);
}

[Test]
public async Task NullableString_WithStringComparison_NoWarning()
{
string? actualValue = "TEST";
string? expectedValue = "test";

// Nullable with StringComparison parameter
await Assert.That(actualValue).IsEqualTo(expectedValue, StringComparison.OrdinalIgnoreCase);
}

[Test]
public async Task NonNullableString_WithModifiers_NoWarning()
{
string actualValue = " TEST ";
string? expectedValue = "test";

// Non-nullable with modifiers and nullable expected
await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringCase();
}

[Test]
public async Task NullableString_WithModifiers_NoWarning()
{
string? actualValue = " TEST ";
string? expectedValue = "test";

// Nullable with modifiers
await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringCase();
}

[Test]
public async Task MixedNullability_FromMethods_NoWarning()
{
// Testing that method return values work correctly
string nonNullable = GetNonNullableString();
string? nullable = GetNullableString();

await Assert.That(nonNullable).IsEqualTo(nullable);
await Assert.That(nullable).IsEqualTo(nonNullable);
}

[Test]
public async Task ImplicitConversion_NonNullableToNullable_NoWarning()
{
// This verifies that the generated extension method signature
// IAssertionSource<string?> accepts both string and string? without warnings
string nonNullable = "test";

// The implicit conversion from string to string? should work seamlessly
await Assert.That(nonNullable).IsEqualTo("test");
await Assert.That(nonNullable).IsEqualTo((string?)"test");
}

private static string GetNonNullableString() => "result";

private static string? GetNullableString() => "result";
}
Loading
Loading