From 9f067981162cf2d15d54ef0c74bf6d8756bcef34 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 21:29:50 +0000
Subject: [PATCH 1/2] Fix GenerateAssertion to preserve parameter default
values in generated extension methods
Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
---
.../MethodAssertionGeneratorTests.cs | 17 +++++++
.../TestData/MethodWithDefaultValues.cs | 16 ++++++
.../Generators/MethodAssertionGenerator.cs | 50 ++++++++++++++++++-
3 files changed, 82 insertions(+), 1 deletion(-)
create mode 100644 TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithDefaultValues.cs
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
index 0f331b922e..ea3d293e50 100644
--- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
@@ -202,6 +202,23 @@ public Task FileScopedClassWithInlining() => RunTest(
await Assert.That(generatedFiles.Count).IsGreaterThanOrEqualTo(1);
});
+ [Test]
+ public Task MethodWithDefaultValues() => RunTest(
+ Path.Combine(Sourcy.Git.RootDirectory.FullName,
+ "TUnit.Assertions.SourceGenerator.Tests",
+ "TestData",
+ "MethodWithDefaultValues.cs"),
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles).HasCount(1);
+
+ var mainFile = generatedFiles.First();
+ await Assert.That(mainFile).IsNotNull();
+
+ // Verify that the default value is preserved in the extension method
+ await Assert.That(mainFile).Contains("bool exact = true");
+ });
+
#if NET6_0_OR_GREATER
[Test]
public Task RefStructParameter() => RunTest(
diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithDefaultValues.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithDefaultValues.cs
new file mode 100644
index 0000000000..26379498b1
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/MethodWithDefaultValues.cs
@@ -0,0 +1,16 @@
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Tests.TestData;
+
+///
+/// Test case: Method with default parameter values
+/// Should generate extension method preserving default values
+///
+public static partial class MethodWithDefaultValuesExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to contain message '{needle}'")]
+ public static bool ContainsMessage(this string[] strings, string needle, bool exact = true)
+ {
+ return exact ? strings.Any(x => x == needle) : strings.Any(x => x.Contains(needle));
+ }
+}
diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
index 1262671901..8dac306758 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
@@ -140,6 +140,8 @@ private static (AssertionMethodData? Data, Diagnostic? Diagnostic) GetAssertionM
IsParams = p.IsParams,
IsInterpolatedStringHandler = IsInterpolatedStringHandler(p.Type),
SimpleTypeName = GetSimpleTypeName(p.Type),
+ HasExplicitDefaultValue = p.HasExplicitDefaultValue,
+ DefaultValueExpression = p.HasExplicitDefaultValue ? FormatDefaultValue(p.ExplicitDefaultValue, p.Type) : null,
}).ToImmutableEquatableArray();
// Extract custom expectation message and inlining preference if provided
@@ -926,6 +928,10 @@ private static void GenerateExtensionMethod(StringBuilder sb, AssertionMethodDat
{
var paramsModifier = param.IsParams ? "params " : "";
sb.Append($", {paramsModifier}{param.Type} {param.Name}");
+ if (param.HasExplicitDefaultValue)
+ {
+ sb.Append($" = {param.DefaultValueExpression}");
+ }
}
// CallerArgumentExpression parameters (skip for params since params must be last)
@@ -1065,6 +1071,46 @@ private static string GetSimpleTypeName(ITypeSymbol type)
return simpleName;
}
+ private static string FormatDefaultValue(object? defaultValue, ITypeSymbol type)
+ {
+ if (defaultValue == null)
+ {
+ return "null";
+ }
+
+ if (type.TypeKind == TypeKind.Enum && type is INamedTypeSymbol enumType)
+ {
+ foreach (var member in enumType.GetMembers())
+ {
+ if (member is IFieldSymbol { HasConstantValue: true } field &&
+ field.ConstantValue != null &&
+ field.ConstantValue.Equals(defaultValue))
+ {
+ return $"{enumType.ToDisplayString()}.{field.Name}";
+ }
+ }
+
+ return $"({enumType.ToDisplayString()})({defaultValue})";
+ }
+
+ if (defaultValue is string str)
+ {
+ return $"\"{str.Replace("\"", "\\\"")}\"";
+ }
+
+ if (defaultValue is bool b)
+ {
+ return b ? "true" : "false";
+ }
+
+ if (defaultValue is char c)
+ {
+ return $"'{c}'";
+ }
+
+ return defaultValue.ToString() ?? "null";
+ }
+
///
/// Collects generic constraints from method type parameters.
/// Returns a list of constraint strings in the format "where T : constraint1, constraint2"
@@ -1212,6 +1258,8 @@ private record struct ParameterData(
bool IsRefStruct,
bool IsParams,
bool IsInterpolatedStringHandler,
- string SimpleTypeName
+ string SimpleTypeName,
+ bool HasExplicitDefaultValue,
+ string? DefaultValueExpression
);
}
From 97875c3ceb8626191e1cf21407c207c2836c35c2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 22:04:08 +0000
Subject: [PATCH 2/2] Add snapshot files for MethodWithDefaultValues test
Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
---
...dWithDefaultValues.DotNet10_0.verified.txt | 67 +++++++++++++++++++
...odWithDefaultValues.DotNet8_0.verified.txt | 67 +++++++++++++++++++
...odWithDefaultValues.DotNet9_0.verified.txt | 67 +++++++++++++++++++
...ethodWithDefaultValues.Net4_7.verified.txt | 67 +++++++++++++++++++
4 files changed, 268 insertions(+)
create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet10_0.verified.txt
create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet8_0.verified.txt
create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet9_0.verified.txt
create mode 100644 TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.Net4_7.verified.txt
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet10_0.verified.txt
new file mode 100644
index 0000000000..09e708af86
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet10_0.verified.txt
@@ -0,0 +1,67 @@
+[
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using TUnit.Assertions.Core;
+using TUnit.Assertions.Tests.TestData;
+
+namespace TUnit.Assertions.Extensions;
+
+///
+/// Generated assertion for ContainsMessage
+///
+public sealed class _ContainsMessage_String_Bool_Assertion : Assertion
+{
+ private readonly string _needle;
+ private readonly bool _exact;
+
+ public _ContainsMessage_String_Bool_Assertion(AssertionContext context, string needle, bool exact)
+ : base(context)
+ {
+ _needle = needle;
+ _exact = exact;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ var value = metadata.Value;
+ var exception = metadata.Exception;
+
+ if (exception != null)
+ {
+ return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}"));
+ }
+
+ if (value is null)
+ {
+ return Task.FromResult(AssertionResult.Failed("Actual value is null"));
+ }
+
+ var result = value!.ContainsMessage(_needle, _exact);
+ return Task.FromResult(result
+ ? AssertionResult.Passed
+ : AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to contain message '{_needle}'";
+ }
+}
+
+public static partial class MethodWithDefaultValuesExtensions
+{
+ ///
+ /// Generated extension method for ContainsMessage
+ ///
+ public static _ContainsMessage_String_Bool_Assertion ContainsMessage(this IAssertionSource source, string needle, bool exact = true, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null, [CallerArgumentExpression(nameof(exact))] string? exactExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsMessage({needleExpression}, {exactExpression})");
+ return new _ContainsMessage_String_Bool_Assertion(source.Context, needle, exact);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet8_0.verified.txt
new file mode 100644
index 0000000000..09e708af86
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet8_0.verified.txt
@@ -0,0 +1,67 @@
+[
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using TUnit.Assertions.Core;
+using TUnit.Assertions.Tests.TestData;
+
+namespace TUnit.Assertions.Extensions;
+
+///
+/// Generated assertion for ContainsMessage
+///
+public sealed class _ContainsMessage_String_Bool_Assertion : Assertion
+{
+ private readonly string _needle;
+ private readonly bool _exact;
+
+ public _ContainsMessage_String_Bool_Assertion(AssertionContext context, string needle, bool exact)
+ : base(context)
+ {
+ _needle = needle;
+ _exact = exact;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ var value = metadata.Value;
+ var exception = metadata.Exception;
+
+ if (exception != null)
+ {
+ return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}"));
+ }
+
+ if (value is null)
+ {
+ return Task.FromResult(AssertionResult.Failed("Actual value is null"));
+ }
+
+ var result = value!.ContainsMessage(_needle, _exact);
+ return Task.FromResult(result
+ ? AssertionResult.Passed
+ : AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to contain message '{_needle}'";
+ }
+}
+
+public static partial class MethodWithDefaultValuesExtensions
+{
+ ///
+ /// Generated extension method for ContainsMessage
+ ///
+ public static _ContainsMessage_String_Bool_Assertion ContainsMessage(this IAssertionSource source, string needle, bool exact = true, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null, [CallerArgumentExpression(nameof(exact))] string? exactExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsMessage({needleExpression}, {exactExpression})");
+ return new _ContainsMessage_String_Bool_Assertion(source.Context, needle, exact);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet9_0.verified.txt
new file mode 100644
index 0000000000..09e708af86
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.DotNet9_0.verified.txt
@@ -0,0 +1,67 @@
+[
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using TUnit.Assertions.Core;
+using TUnit.Assertions.Tests.TestData;
+
+namespace TUnit.Assertions.Extensions;
+
+///
+/// Generated assertion for ContainsMessage
+///
+public sealed class _ContainsMessage_String_Bool_Assertion : Assertion
+{
+ private readonly string _needle;
+ private readonly bool _exact;
+
+ public _ContainsMessage_String_Bool_Assertion(AssertionContext context, string needle, bool exact)
+ : base(context)
+ {
+ _needle = needle;
+ _exact = exact;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ var value = metadata.Value;
+ var exception = metadata.Exception;
+
+ if (exception != null)
+ {
+ return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}"));
+ }
+
+ if (value is null)
+ {
+ return Task.FromResult(AssertionResult.Failed("Actual value is null"));
+ }
+
+ var result = value!.ContainsMessage(_needle, _exact);
+ return Task.FromResult(result
+ ? AssertionResult.Passed
+ : AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to contain message '{_needle}'";
+ }
+}
+
+public static partial class MethodWithDefaultValuesExtensions
+{
+ ///
+ /// Generated extension method for ContainsMessage
+ ///
+ public static _ContainsMessage_String_Bool_Assertion ContainsMessage(this IAssertionSource source, string needle, bool exact = true, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null, [CallerArgumentExpression(nameof(exact))] string? exactExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsMessage({needleExpression}, {exactExpression})");
+ return new _ContainsMessage_String_Bool_Assertion(source.Context, needle, exact);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.Net4_7.verified.txt
new file mode 100644
index 0000000000..09e708af86
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.MethodWithDefaultValues.Net4_7.verified.txt
@@ -0,0 +1,67 @@
+[
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using TUnit.Assertions.Core;
+using TUnit.Assertions.Tests.TestData;
+
+namespace TUnit.Assertions.Extensions;
+
+///
+/// Generated assertion for ContainsMessage
+///
+public sealed class _ContainsMessage_String_Bool_Assertion : Assertion
+{
+ private readonly string _needle;
+ private readonly bool _exact;
+
+ public _ContainsMessage_String_Bool_Assertion(AssertionContext context, string needle, bool exact)
+ : base(context)
+ {
+ _needle = needle;
+ _exact = exact;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ var value = metadata.Value;
+ var exception = metadata.Exception;
+
+ if (exception != null)
+ {
+ return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().FullName}"));
+ }
+
+ if (value is null)
+ {
+ return Task.FromResult(AssertionResult.Failed("Actual value is null"));
+ }
+
+ var result = value!.ContainsMessage(_needle, _exact);
+ return Task.FromResult(result
+ ? AssertionResult.Passed
+ : AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to contain message '{_needle}'";
+ }
+}
+
+public static partial class MethodWithDefaultValuesExtensions
+{
+ ///
+ /// Generated extension method for ContainsMessage
+ ///
+ public static _ContainsMessage_String_Bool_Assertion ContainsMessage(this IAssertionSource source, string needle, bool exact = true, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null, [CallerArgumentExpression(nameof(exact))] string? exactExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsMessage({needleExpression}, {exactExpression})");
+ return new _ContainsMessage_String_Bool_Assertion(source.Context, needle, exact);
+ }
+
+}
+
+]
\ No newline at end of file