diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet10_0.verified.txt
new file mode 100644
index 0000000000..5585abcba2
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet10_0.verified.txt
@@ -0,0 +1,33 @@
+[
+//
+#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;
+
+///
+/// Generated extension methods for ConcreteReceiverWithExtraGenericAssertion.
+///
+public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
+{
+
+ ///
+ /// Extension method for ConcreteReceiverWithExtraGenericAssertion.
+ ///
+ public static ConcreteReceiverWithExtraGenericAssertion ConcreteReceiverWithExtraGenericMatches(this IAssertionSource source, System.Func predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
+ where TActual : System.Exception
+ {
+ source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
+ source.Context.ExpressionBuilder.Append(predicateExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new ConcreteReceiverWithExtraGenericAssertion(source.Context.Map(static x => (System.Exception?)x), predicate);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet8_0.verified.txt
new file mode 100644
index 0000000000..5585abcba2
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet8_0.verified.txt
@@ -0,0 +1,33 @@
+[
+//
+#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;
+
+///
+/// Generated extension methods for ConcreteReceiverWithExtraGenericAssertion.
+///
+public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
+{
+
+ ///
+ /// Extension method for ConcreteReceiverWithExtraGenericAssertion.
+ ///
+ public static ConcreteReceiverWithExtraGenericAssertion ConcreteReceiverWithExtraGenericMatches(this IAssertionSource source, System.Func predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
+ where TActual : System.Exception
+ {
+ source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
+ source.Context.ExpressionBuilder.Append(predicateExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new ConcreteReceiverWithExtraGenericAssertion(source.Context.Map(static x => (System.Exception?)x), predicate);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet9_0.verified.txt
new file mode 100644
index 0000000000..5585abcba2
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.DotNet9_0.verified.txt
@@ -0,0 +1,33 @@
+[
+//
+#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;
+
+///
+/// Generated extension methods for ConcreteReceiverWithExtraGenericAssertion.
+///
+public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
+{
+
+ ///
+ /// Extension method for ConcreteReceiverWithExtraGenericAssertion.
+ ///
+ public static ConcreteReceiverWithExtraGenericAssertion ConcreteReceiverWithExtraGenericMatches(this IAssertionSource source, System.Func predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
+ where TActual : System.Exception
+ {
+ source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
+ source.Context.ExpressionBuilder.Append(predicateExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new ConcreteReceiverWithExtraGenericAssertion(source.Context.Map(static x => (System.Exception?)x), predicate);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.Net4_7.verified.txt
new file mode 100644
index 0000000000..5585abcba2
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric.Net4_7.verified.txt
@@ -0,0 +1,33 @@
+[
+//
+#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;
+
+///
+/// Generated extension methods for ConcreteReceiverWithExtraGenericAssertion.
+///
+public static partial class ConcreteReceiverWithExtraGenericAssertionExtensions
+{
+
+ ///
+ /// Extension method for ConcreteReceiverWithExtraGenericAssertion.
+ ///
+ public static ConcreteReceiverWithExtraGenericAssertion ConcreteReceiverWithExtraGenericMatches(this IAssertionSource source, System.Func predicate, [CallerArgumentExpression(nameof(predicate))] string? predicateExpression = null)
+ where TActual : System.Exception
+ {
+ source.Context.ExpressionBuilder.Append(".ConcreteReceiverWithExtraGenericMatches(");
+ source.Context.ExpressionBuilder.Append(predicateExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new ConcreteReceiverWithExtraGenericAssertion(source.Context.Map(static x => (System.Exception?)x), predicate);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs
index 95b8029a5e..7ecadf520d 100644
--- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs
@@ -1,3 +1,5 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using TUnit.Assertions.SourceGenerator.Generators;
using TUnit.Assertions.SourceGenerator.Tests.Options;
@@ -49,6 +51,58 @@ public Task MultipleGenericParameters() => RunTest(
await Assert.That(extensionFile!).Contains("");
});
+ [Test]
+ public Task ConcreteReceiverWithExtraGeneric() => RunTest(
+ Path.Combine(Sourcy.Git.RootDirectory.FullName,
+ "TUnit.Assertions.SourceGenerator.Tests",
+ "TestData",
+ "ConcreteReceiverWithExtraGenericAssertion.cs"),
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles).Count().IsEqualTo(1);
+ var extensionFile = generatedFiles.FirstOrDefault(f => f.Contains("ConcreteReceiverWithExtraGenericMatches"));
+ await Assert.That(extensionFile).IsNotNull();
+ await Assert.That(extensionFile!).Contains("ConcreteReceiverWithExtraGenericMatches");
+
+ // The covariant receiver-type parameter (if applied) and the class's own type
+ // parameter must appear in a single merged generic parameter list. Two adjacent
+ // blocks ( or ) is invalid C# syntax. The compile-clean check
+ // below is the structural guard; this surface-level assertion pins the absence
+ // of adjacent generic-parameter blocks regardless of whether covariance fired
+ // for this fixture's compilation context.
+ await Assert.That(extensionFile!).DoesNotContain("><");
+
+ // 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)
+ // regardless of specific diagnostic id.
+ var trees = generatedFiles
+ .Select(source => CSharpSyntaxTree.ParseText(source))
+ .ToArray();
+
+ var compilation = CSharpCompilation.Create(
+ "CompileCheck",
+ trees,
+ ReferencesHelper.References,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ // On the net472 leg of CI, the Polyfill assembly's CallerArgumentExpressionAttribute
+ // is declared internal, which produces a CS0122 ('inaccessible due to its protection
+ // level') false positive when Roslyn compiles the generator's output through the test
+ // project's reference set. Filter CS0122 only on net472; the emit shape is still
+ // pinned by the `.Net4_7` snapshot file. On modern TFMs the BCL attribute is public,
+ // so CS0122 (if it ever appears) would be a genuine signal and is not filtered.
+ var errors = compilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+#if NETFRAMEWORK
+ .Where(d => !string.Equals(d.Id, "CS0122", StringComparison.Ordinal))
+#endif
+ .Select(d => $"{d.Id}: {d.GetMessage()}")
+ .ToArray();
+
+ await Assert.That(errors).IsEmpty();
+ });
+
[Test]
public Task AssertionWithOptionalParameter() => RunTest(
Path.Combine(Sourcy.Git.RootDirectory.FullName,
diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/ConcreteReceiverWithExtraGenericAssertion.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/ConcreteReceiverWithExtraGenericAssertion.cs
new file mode 100644
index 0000000000..474737ce20
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/ConcreteReceiverWithExtraGenericAssertion.cs
@@ -0,0 +1,32 @@
+using TUnit.Assertions.Attributes;
+using TUnit.Assertions.Core;
+
+namespace TUnit.Assertions.Tests.TestData;
+
+/// Test case: an -decorated
+/// subclass over a CONCRETE receiver type that ALSO declares
+/// its own generic type parameter on the class. The generated extension method must merge
+/// both the covariant receiver-type parameter and the class's own type parameter into a
+/// single generic parameter list (e.g. <TActual, T>), NOT emit them as two
+/// adjacent blocks (<TActual><T>) which is invalid C# syntax.
+/// Uses as the receiver to anchor the fixture to a BCL
+/// non-sealed class (the covariance candidate the generator looks for) without adding
+/// a stand-in type to the test surface.
+[AssertionExtension("ConcreteReceiverWithExtraGenericMatches")]
+public class ConcreteReceiverWithExtraGenericAssertion : Assertion
+{
+ private readonly Func _predicate;
+
+ public ConcreteReceiverWithExtraGenericAssertion(AssertionContext context, Func predicate)
+ : base(context)
+ {
+ _predicate = predicate;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ return Task.FromResult(AssertionResult.Passed);
+ }
+
+ protected override string GetExpectation() => "to match the predicate";
+}
diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
index 929563d84a..a3f3fed290 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
@@ -384,11 +384,22 @@ private static void GenerateExtensionMethod(
}
sourceBuilder.Append($" public static {returnType} {methodName}");
+
+ // Merge the covariant receiver-type parameter (if any) with the assertion class's own
+ // type parameters into a single generic parameter list. Emitting them as two adjacent
+ // blocks produces invalid C# when both are present (e.g. an Assertion
+ // subclass that also declares its own : the receiver-type covariance adds TActual,
+ // the class adds T, and the method signature must be , not ).
+ var methodGenericParams = new List();
if (genericTypeParam != null)
{
- sourceBuilder.Append($"<{genericTypeParam}>");
+ methodGenericParams.Add(genericTypeParam);
+ }
+ methodGenericParams.AddRange(genericParams);
+ if (methodGenericParams.Count > 0)
+ {
+ sourceBuilder.Append($"<{string.Join(", ", methodGenericParams)}>");
}
- sourceBuilder.Append(genericParamsString);
sourceBuilder.Append("(");
sourceBuilder.Append($"this {sourceType} source");