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 @@ -28,6 +28,17 @@ public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), predicate);
}

/// <summary>
/// Extension method for ConcreteReceiverWithExtraGenericAssertion.
/// </summary>
public static ConcreteReceiverWithExtraGenericAssertion<T> ConcreteReceiverWithExtraGenericMatches<T>(this IAssertionSource<System.Exception> source, System.Func<T, bool> predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
source.Context.ExpressionBuilder.Append(predicateExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context, predicate);
}
}

]
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), predicate);
}

/// <summary>
/// Extension method for ConcreteReceiverWithExtraGenericAssertion.
/// </summary>
public static ConcreteReceiverWithExtraGenericAssertion<T> ConcreteReceiverWithExtraGenericMatches<T>(this IAssertionSource<System.Exception> source, System.Func<T, bool> predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
source.Context.ExpressionBuilder.Append(predicateExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context, predicate);
}
}

]
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), predicate);
}

/// <summary>
/// Extension method for ConcreteReceiverWithExtraGenericAssertion.
/// </summary>
public static ConcreteReceiverWithExtraGenericAssertion<T> ConcreteReceiverWithExtraGenericMatches<T>(this IAssertionSource<System.Exception> source, System.Func<T, bool> predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
source.Context.ExpressionBuilder.Append(predicateExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context, predicate);
}
}

]
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), predicate);
}

/// <summary>
/// Extension method for ConcreteReceiverWithExtraGenericAssertion.
/// </summary>
public static ConcreteReceiverWithExtraGenericAssertion<T> ConcreteReceiverWithExtraGenericMatches<T>(this IAssertionSource<System.Exception> source, System.Func<T, bool> predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
source.Context.ExpressionBuilder.Append(predicateExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithExtraGenericAssertion<T>(source.Context, predicate);
}
}

]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
// <auto-generated/>
#pragma warning disable
#nullable enable

using System;
using System.Runtime.CompilerServices;
using TUnit.Assertions.Core;
using TUnit.Assertions.Enums;
using TUnit.Assertions.Tests.TestData;

namespace TUnit.Assertions.Extensions;

/// <summary>
/// Generated extension methods for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static partial class ConcreteReceiverWithInferableGenericAssertionExtensions
{

/// <summary>
/// Extension method for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static ConcreteReceiverWithInferableGenericAssertion<T> ConcreteReceiverWithInferableGenericTagged<TActual, T>(this IAssertionSource<TActual> source, T tag, [CallerArgumentExpression(nameof(tag))] string? tagExpression = null)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithInferableGenericTagged(");
source.Context.ExpressionBuilder.Append(tagExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithInferableGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), tag);
}
}

]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
// <auto-generated/>
#pragma warning disable
#nullable enable

using System;
using System.Runtime.CompilerServices;
using TUnit.Assertions.Core;
using TUnit.Assertions.Enums;
using TUnit.Assertions.Tests.TestData;

namespace TUnit.Assertions.Extensions;

/// <summary>
/// Generated extension methods for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static partial class ConcreteReceiverWithInferableGenericAssertionExtensions
{

/// <summary>
/// Extension method for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static ConcreteReceiverWithInferableGenericAssertion<T> ConcreteReceiverWithInferableGenericTagged<TActual, T>(this IAssertionSource<TActual> source, T tag, [CallerArgumentExpression(nameof(tag))] string? tagExpression = null)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithInferableGenericTagged(");
source.Context.ExpressionBuilder.Append(tagExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithInferableGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), tag);
}
}

]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
// <auto-generated/>
#pragma warning disable
#nullable enable

using System;
using System.Runtime.CompilerServices;
using TUnit.Assertions.Core;
using TUnit.Assertions.Enums;
using TUnit.Assertions.Tests.TestData;

namespace TUnit.Assertions.Extensions;

/// <summary>
/// Generated extension methods for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static partial class ConcreteReceiverWithInferableGenericAssertionExtensions
{

/// <summary>
/// Extension method for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static ConcreteReceiverWithInferableGenericAssertion<T> ConcreteReceiverWithInferableGenericTagged<TActual, T>(this IAssertionSource<TActual> source, T tag, [CallerArgumentExpression(nameof(tag))] string? tagExpression = null)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithInferableGenericTagged(");
source.Context.ExpressionBuilder.Append(tagExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithInferableGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), tag);
}
}

]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
// <auto-generated/>
#pragma warning disable
#nullable enable

using System;
using System.Runtime.CompilerServices;
using TUnit.Assertions.Core;
using TUnit.Assertions.Enums;
using TUnit.Assertions.Tests.TestData;

namespace TUnit.Assertions.Extensions;

/// <summary>
/// Generated extension methods for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static partial class ConcreteReceiverWithInferableGenericAssertionExtensions
{

/// <summary>
/// Extension method for ConcreteReceiverWithInferableGenericAssertion.
/// </summary>
public static ConcreteReceiverWithInferableGenericAssertion<T> ConcreteReceiverWithInferableGenericTagged<TActual, T>(this IAssertionSource<TActual> source, T tag, [CallerArgumentExpression(nameof(tag))] string? tagExpression = null)
where TActual : System.Exception
{
source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithInferableGenericTagged(");
source.Context.ExpressionBuilder.Append(tagExpression);
source.Context.ExpressionBuilder.Append(")");
return new ConcreteReceiverWithInferableGenericAssertion<T>(source.Context.Map<System.Exception>(static x => (System.Exception?)x), tag);
}
}

]
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ public Task ConcreteReceiverWithExtraGeneric() => RunTest(
// for this fixture's compilation context.
await Assert.That(extensionFile!).DoesNotContain("><");

// Issue #5922: for a covariance-candidate receiver that also declares its own generic
// parameter, the generator emits BOTH the covariant overload (keeps subclass binding)
// and an inference-friendly pinned-receiver overload that omits the covariant TActual,
// so the common exact-type call site only needs the class's own type argument.
// Covariant overload: <TActual, T> receiver IAssertionSource<TActual>.
await Assert.That(extensionFile!).Contains("ConcreteReceiverWithExtraGenericMatches<TActual, T>(this IAssertionSource<TActual> source");
// Pinned overload: <T> only, receiver pinned to the concrete IAssertionSource<System.Exception>.
await Assert.That(extensionFile!).Contains("ConcreteReceiverWithExtraGenericMatches<T>(this IAssertionSource<System.Exception> source");

// Compile-clean gate: parse + compile the generated source through Roslyn and
// assert no error-severity diagnostic. Catches the entire class of emit-syntax
// bugs (mis-paired brackets, wrong default rendering, adjacent generic blocks)
Expand Down Expand Up @@ -103,6 +112,26 @@ public Task ConcreteReceiverWithExtraGeneric() => RunTest(
await Assert.That(errors).IsEmpty();
});

[Test]
public Task ConcreteReceiverWithInferableGeneric() => RunTest(
Path.Combine(Sourcy.Git.RootDirectory.FullName,
"TUnit.Assertions.SourceGenerator.Tests",
"TestData",
"ConcreteReceiverWithInferableGenericAssertion.cs"),
async generatedFiles =>
{
await Assert.That(generatedFiles).Count().IsEqualTo(1);
var extensionFile = generatedFiles.FirstOrDefault(f => f.Contains("ConcreteReceiverWithInferableGenericTagged"));
await Assert.That(extensionFile).IsNotNull();

// Issue #5922: the own type parameter T is inferable from the `T tag` value argument, so
// the caller never names a type argument and the covariant overload binds on its own.
// Covariant overload IS emitted (keeps subclass binding).
await Assert.That(extensionFile!).Contains("ConcreteReceiverWithInferableGenericTagged<TActual, T>(this IAssertionSource<TActual> source");
// The pinned-receiver overload would be pure dead weight here, so it must NOT be emitted.
await Assert.That(extensionFile!).DoesNotContain("(this IAssertionSource<System.Exception> source");
});

[Test]
public Task AssertionWithOptionalParameter() => RunTest(
Path.Combine(Sourcy.Git.RootDirectory.FullName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using TUnit.Assertions.Attributes;
using TUnit.Assertions.Core;

namespace TUnit.Assertions.Tests.TestData;

/// <summary>Test case: an <see cref="AssertionExtensionAttribute"/>-decorated
/// <see cref="Assertion{T}"/> subclass over a CONCRETE, covariance-candidate receiver type that
/// ALSO declares its own generic type parameter — but whose own parameter <c>T</c> is INFERABLE
/// from a plain value argument (<c>T tag</c>). Because the caller never has to name a type
/// argument, the covariant <c>&lt;TActual, T&gt;</c> overload binds on its own and the
/// inference-friendly pinned-receiver overload would be redundant, so the generator must NOT emit
/// it. Contrast with <c>ConcreteReceiverWithExtraGenericAssertion</c>, whose <c>Func&lt;T, bool&gt;</c>
/// parameter makes <c>T</c> non-inferable and so DOES get the pinned overload. See issue #5922.</summary>
[AssertionExtension("ConcreteReceiverWithInferableGenericTagged")]
public class ConcreteReceiverWithInferableGenericAssertion<T> : Assertion<System.Exception>
{
private readonly T _tag;

public ConcreteReceiverWithInferableGenericAssertion(AssertionContext<System.Exception> context, T tag)
: base(context)
{
_tag = tag;
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<System.Exception> metadata)
{
return Task.FromResult(AssertionResult.Passed);
}

protected override string GetExpectation() => "to be tagged";
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As
sourceBuilder.AppendLine($"public static partial class {extensionClassName}");
sourceBuilder.AppendLine("{");

// When the receiver type is a covariance candidate AND the assertion class declares its
// own generic parameter(s), the covariant method signature is <TActual, T...>. Callers
// that name the class's own arguments (e.g. a non-inferable lambda type) must then also
// spell out the covariant TActual, because C# forbids partial type-argument specification.
// To restore inference for the common exact-receiver call site, additionally emit a
// pinned-receiver overload (IAssertionSource<TConcrete>) that omits TActual. The covariant
// overload is kept so a more-derived static receiver can still bind. See issue #5922.
//
// The pinned overload is only emitted when at least one of the class's own type parameters
// is NOT inferable from the constructor's value arguments. If every own type parameter is
// inferable (e.g. a `T tag` value parameter), the caller writes no type arguments at all and
// the covariant overload binds on its own — the pinned overload would be pure dead weight.
// Excluding that case also keeps the two overloads arity-disjoint (pinned <T...> vs covariant
// <TActual, T...>), so they are never both applicable and no OverloadResolutionPriority
// tiebreaker is required.
var receiverType = data.AssertionBaseType.TypeArguments[0];
var hasOwnGenerics = data.ClassSymbol.IsGenericType && data.ClassSymbol.TypeParameters.Length > 0;
var receiverIsCovariantCandidate = CovarianceHelper.IsCovariantCandidate(receiverType) && hasOwnGenerics;
var ownTypeParameters = data.ClassSymbol.TypeParameters;

// Generate extension methods for each constructor
foreach (var constructor in data.Constructors)
{
Expand All @@ -172,13 +192,18 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As
continue;
}

// The pinned overload only earns its place when a non-inferable own type parameter forces
// the caller to name type arguments; otherwise it is redundant with the covariant overload.
var emitPinned = receiverIsCovariantCandidate
&& !CovarianceHelper.OwnGenericsAreInferable(constructor, ownTypeParameters);

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

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

Expand All @@ -189,6 +214,24 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As
context.AddSource(fileName, sourceBuilder.ToString());
}

/// <summary>
/// Emits the covariant extension method and, when <paramref name="emitPinned"/> is set, the
/// inference-friendly pinned-receiver overload alongside it.
/// </summary>
private static void EmitMethod(
StringBuilder sourceBuilder,
AssertionExtensionData data,
IMethodSymbol constructor,
bool negated,
bool emitPinned)
{
GenerateExtensionMethod(sourceBuilder, data, constructor, negated, isNullableOverload: false);
if (emitPinned)
{
GenerateExtensionMethod(sourceBuilder, data, constructor, negated, isNullableOverload: false, pinnedReceiver: true);
}
}

private static bool IsValidConstructor(IMethodSymbol constructor)
{
// Must have at least one parameter
Expand All @@ -214,7 +257,8 @@ private static void GenerateExtensionMethod(
AssertionExtensionData data,
IMethodSymbol constructor,
bool negated,
bool isNullableOverload)
bool isNullableOverload,
bool pinnedReceiver = false)
{
var methodName = negated ? data.NegatedMethodName : data.MethodName;
var assertionType = data.ClassSymbol;
Expand Down Expand Up @@ -331,7 +375,10 @@ private static void GenerateExtensionMethod(

// 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
// so they're preferred over the base nullable overload when source is non-nullable.
// The pinned-receiver overload intentionally shares the same priority as its covariant
// sibling (the priority is not pinnedReceiver-aware): the two are arity-disjoint and never
// both applicable, so the C# "more specific receiver" rule is the tiebreaker, not ORP.
var effectivePriority = data.OverloadResolutionPriority;
if (isNullableOverload)
{
Expand All @@ -354,7 +401,9 @@ private static void GenerateExtensionMethod(
string? genericTypeParam = null;
string? genericConstraint = null;

var isCovariantCandidate = !isNullableOverload
// pinnedReceiver suppresses covariance so the receiver is pinned to the concrete type
// (IAssertionSource<TConcrete>), dropping the TActual parameter for inference-friendly call sites.
var isCovariantCandidate = !isNullableOverload && !pinnedReceiver
&& CovarianceHelper.IsCovariantCandidate(typeParam);
var typeParamDisplay = typeParam.ToDisplayString();

Expand Down
Loading
Loading