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 @@ -406,93 +406,113 @@ public static partial class ExceptionAssertionExtensions
/// <summary>
/// Generated extension method for HasInnerException
/// </summary>
public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasInnerException_Assertion HasInnerException<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasInnerException()");
return new Exception_HasInnerException_Assertion(source.Context);
return new Exception_HasInnerException_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasNoInnerException
/// </summary>
public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasNoInnerException_Assertion HasNoInnerException<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasNoInnerException()");
return new Exception_HasNoInnerException_Assertion(source.Context);
return new Exception_HasNoInnerException_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasStackTrace
/// </summary>
public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasStackTrace_Assertion HasStackTrace<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasStackTrace()");
return new Exception_HasStackTrace_Assertion(source.Context);
return new Exception_HasStackTrace_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasNoData
/// </summary>
public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasNoData_Assertion HasNoData<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasNoData()");
return new Exception_HasNoData_Assertion(source.Context);
return new Exception_HasNoData_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasHelpLink
/// </summary>
public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasHelpLink_Assertion HasHelpLink<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasHelpLink()");
return new Exception_HasHelpLink_Assertion(source.Context);
return new Exception_HasHelpLink_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasNoHelpLink
/// </summary>
public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasNoHelpLink_Assertion HasNoHelpLink<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasNoHelpLink()");
return new Exception_HasNoHelpLink_Assertion(source.Context);
return new Exception_HasNoHelpLink_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasSource
/// </summary>
public static Exception_HasSource_Assertion HasSource(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasSource_Assertion HasSource<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasSource()");
return new Exception_HasSource_Assertion(source.Context);
return new Exception_HasSource_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasNoSource
/// </summary>
public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource<System.Exception> source)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static Exception_HasNoSource_Assertion HasNoSource<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasNoSource()");
return new Exception_HasNoSource_Assertion(source.Context);
return new Exception_HasNoSource_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasTargetSite
/// </summary>
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Exception.TargetSite uses reflection which may be trimmed in AOT scenarios")]
public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource<System.Exception> source)
public static Exception_HasTargetSite_Assertion HasTargetSite<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasTargetSite()");
return new Exception_HasTargetSite_Assertion(source.Context);
return new Exception_HasTargetSite_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

/// <summary>
/// Generated extension method for HasNoTargetSite
/// </summary>
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Exception.TargetSite uses reflection which may be trimmed in AOT scenarios")]
public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource<System.Exception> source)
public static Exception_HasNoTargetSite_Assertion HasNoTargetSite<TActual>(this IAssertionSource<TActual> source)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".HasNoTargetSite()");
return new Exception_HasNoTargetSite_Assertion(source.Context);
return new Exception_HasNoTargetSite_Assertion(source.Context.Map<System.Exception>(static x => (System.Exception?)x));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ public static partial class AssertionResultOfTMethodExtensions
/// <summary>
/// Generated extension method for ContainsMatch
/// </summary>
public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource<System.Collections.Generic.IEnumerable<string>> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch<TActual>(this IAssertionSource<TActual> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null)
where TActual : System.Collections.Generic.IEnumerable<string>
{
source.Context.ExpressionBuilder.Append($".ContainsMatch({needleExpression})");
return new IEnumerableString_ContainsMatch_String_Assertion(source.Context, needle);
return new IEnumerableString_ContainsMatch_String_Assertion(source.Context.Map<System.Collections.Generic.IEnumerable<string>>(static x => (System.Collections.Generic.IEnumerable<string>?)x), needle);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ public static partial class AsyncAssertionResultOfTMethodExtensions
/// <summary>
/// Generated extension method for ContainsMatchAsync
/// </summary>
public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource<System.Collections.Generic.IEnumerable<string>> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null)
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync<TActual>(this IAssertionSource<TActual> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null)
where TActual : System.Collections.Generic.IEnumerable<string>
{
source.Context.ExpressionBuilder.Append($".ContainsMatchAsync({needleExpression})");
return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context, needle);
return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context.Map<System.Collections.Generic.IEnumerable<string>>(static x => (System.Collections.Generic.IEnumerable<string>?)x), needle);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,6 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As
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, isNullableOverload: false);

Expand Down Expand Up @@ -242,15 +237,9 @@ private static void GenerateExtensionMethod(
requiresUnreferencedCodeMessage = data.RequiresUnreferencedCodeMessage;
}

// Build generic type parameters string
// Use the assertion class's own type parameters if it has them
var genericParams = new List<string>();
var typeConstraints = new List<string>();

// NEW: Detect if this is a multi-parameter generic assertion (e.g., collection assertions)
// Check if the assertion class has multiple type parameters beyond just Assertion<T>
var isMultiParameterGeneric = assertionType.TypeParameters.Length > 1;

if (assertionType.IsGenericType && assertionType.TypeParameters.Length > 0)
{
// The assertion class defines its own generic type parameters
Expand Down Expand Up @@ -360,25 +349,36 @@ private static void GenerateExtensionMethod(
// The extension method extends IAssertionSource<T> where T is the type argument
// from the Assertion<T> base class.
string sourceType;
string genericTypeParam = null;
string genericConstraint = null;
string? genericTypeParam = null;
string? genericConstraint = null;

var isCovariantCandidate = !isNullableOverload
&& CovarianceHelper.IsCovariantCandidate(typeParam);
var typeParamDisplay = typeParam.ToDisplayString();

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()}>";
sourceType = $"IAssertionSource<{typeParamDisplay}>";
genericTypeParam = null;
genericConstraint = null;
}
else if (typeParam is ITypeParameterSymbol baseTypeParam)
{
sourceType = $"IAssertionSource<{baseTypeParam.Name}>";
}
else if (isCovariantCandidate)
{
var covariantParam = CovarianceHelper.GetCovariantTypeParamName(genericParams);
sourceType = $"IAssertionSource<{covariantParam}>";
genericTypeParam = covariantParam;
genericConstraint = $"where {covariantParam} : {CovarianceHelper.GetConstraintTypeName(typeParamDisplay, typeParam)}";
}
else
{
sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>";
sourceType = $"IAssertionSource<{typeParamDisplay}>";
}

sourceBuilder.Append($" public static {returnType} {methodName}");
Expand Down Expand Up @@ -465,6 +465,10 @@ private static void GenerateExtensionMethod(
{
sourceBuilder.Append("source.Context.AsNullable()");
}
else if (isCovariantCandidate)
{
sourceBuilder.Append(CovarianceHelper.GetCovariantContextExpr(typeParamDisplay));
}
else
{
sourceBuilder.Append("source.Context");
Expand Down
95 changes: 95 additions & 0 deletions TUnit.Assertions.SourceGenerator/Generators/CovarianceHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace TUnit.Assertions.SourceGenerator.Generators;

/// <summary>
/// Shared helpers for covariant assertion generation across MethodAssertionGenerator and AssertionExtensionGenerator.
/// </summary>
internal static class CovarianceHelper
{
private const string PreferredName = "TActual";

/// <summary>
/// Returns a type parameter name for the covariant source type that doesn't conflict
/// with any existing generic parameters on the method.
/// </summary>
public static string GetCovariantTypeParamName(IEnumerable<string> existingGenericParams)
{
if (!existingGenericParams.Contains(PreferredName))
{
return PreferredName;
}

var candidate = PreferredName + "_";
while (existingGenericParams.Contains(candidate))
{
candidate += "_";
}
return candidate;
}

/// <summary>
/// Determines if a target type supports covariant assertions.
/// Returns true for interfaces and non-sealed classes that don't contain unresolved type parameters.
/// </summary>
public static bool IsCovariantCandidate(ITypeSymbol type)
{
return (type.TypeKind == TypeKind.Interface || type.TypeKind == TypeKind.Class)
&& !type.IsSealed
&& !ContainsTypeParameter(type);
}

/// <summary>
/// Returns the type name suitable for use in a generic constraint (strips trailing nullable annotation).
/// </summary>
public static string GetConstraintTypeName(string typeName, ITypeSymbol type)
{
if (type.NullableAnnotation == NullableAnnotation.Annotated && typeName.EndsWith("?"))
{
return typeName.Substring(0, typeName.Length - 1);
}
return typeName;
}

/// <summary>
/// Generates the context mapping expression for covariant assertions.
/// Uses nullable cast since Map's Func takes TValue? and returns TNew?.
/// </summary>
public static string GetCovariantContextExpr(string targetTypeName)
{
var nullableCastType = targetTypeName.EndsWith("?") ? targetTypeName : $"{targetTypeName}?";
return $"source.Context.Map<{targetTypeName}>(static x => ({nullableCastType})x)";
}

/// <summary>
/// Recursively checks if a type symbol contains any unresolved type parameters.
/// Types like Lazy&lt;T&gt; would break type inference if made covariant.
/// </summary>
public static bool ContainsTypeParameter(ITypeSymbol type)
{
if (type is ITypeParameterSymbol)
{
return true;
}

if (type is INamedTypeSymbol namedType)
{
foreach (var typeArg in namedType.TypeArguments)
{
if (ContainsTypeParameter(typeArg))
{
return true;
}
}
}

if (type is IArrayTypeSymbol arrayType)
{
return ContainsTypeParameter(arrayType.ElementType);
}

return false;
}
}
Loading
Loading