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