diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet10_0.verified.txt
new file mode 100644
index 0000000000..673964419f
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet10_0.verified.txt
@@ -0,0 +1,32 @@
+[
+//
+#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 StringRespectsTokenAssertion.
+///
+public static partial class StringRespectsTokenAssertionExtensions
+{
+
+ ///
+ /// Extension method for StringRespectsTokenAssertion.
+ ///
+ public static StringRespectsTokenAssertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append(".RespectsToken(");
+ source.Context.ExpressionBuilder.Append(tokenExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new StringRespectsTokenAssertion(source.Context, token);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet8_0.verified.txt
new file mode 100644
index 0000000000..673964419f
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet8_0.verified.txt
@@ -0,0 +1,32 @@
+[
+//
+#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 StringRespectsTokenAssertion.
+///
+public static partial class StringRespectsTokenAssertionExtensions
+{
+
+ ///
+ /// Extension method for StringRespectsTokenAssertion.
+ ///
+ public static StringRespectsTokenAssertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append(".RespectsToken(");
+ source.Context.ExpressionBuilder.Append(tokenExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new StringRespectsTokenAssertion(source.Context, token);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet9_0.verified.txt
new file mode 100644
index 0000000000..673964419f
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.DotNet9_0.verified.txt
@@ -0,0 +1,32 @@
+[
+//
+#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 StringRespectsTokenAssertion.
+///
+public static partial class StringRespectsTokenAssertionExtensions
+{
+
+ ///
+ /// Extension method for StringRespectsTokenAssertion.
+ ///
+ public static StringRespectsTokenAssertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append(".RespectsToken(");
+ source.Context.ExpressionBuilder.Append(tokenExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new StringRespectsTokenAssertion(source.Context, token);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.Net4_7.verified.txt
new file mode 100644
index 0000000000..673964419f
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.ValueTypeDefaultParameter.Net4_7.verified.txt
@@ -0,0 +1,32 @@
+[
+//
+#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 StringRespectsTokenAssertion.
+///
+public static partial class StringRespectsTokenAssertionExtensions
+{
+
+ ///
+ /// Extension method for StringRespectsTokenAssertion.
+ ///
+ public static StringRespectsTokenAssertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append(".RespectsToken(");
+ source.Context.ExpressionBuilder.Append(tokenExpression);
+ source.Context.ExpressionBuilder.Append(")");
+ return new StringRespectsTokenAssertion(source.Context, token);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs
index 7ecadf520d..19ff74a1f5 100644
--- a/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs
+++ b/TUnit.Assertions.SourceGenerator.Tests/AssertionExtensionGeneratorTests.cs
@@ -210,4 +210,27 @@ public Task AssertionWithMultipleParameters() => RunTest(
var callerExprCount = System.Text.RegularExpressions.Regex.Matches(extensionFile!, "CallerArgumentExpression").Count;
await Assert.That(callerExprCount).IsGreaterThanOrEqualTo(2);
});
+
+ [Test]
+ public Task ValueTypeDefaultParameter() => RunTest(
+ Path.Combine(Sourcy.Git.RootDirectory.FullName,
+ "TUnit.Assertions.SourceGenerator.Tests",
+ "TestData",
+ "AssertionExtensionValueTypeDefaultParameterAssertion.cs"),
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles).Count().IsEqualTo(1);
+
+ var extensionFile = generatedFiles.First();
+ await Assert.That(extensionFile).IsNotNull();
+
+ // A non-nullable value-type constructor parameter declared with `= default` must
+ // render as the bare `default` literal, not `= null`. The literal `null` is invalid
+ // for a non-nullable value type and produces CS1750. The trailing comma anchors the
+ // assertion to bare `default`, ruling out the longer `default(TypeName)` form.
+ await Assert.That(extensionFile).Contains("CancellationToken token = default,");
+ await Assert.That(extensionFile).DoesNotContain("CancellationToken token = null");
+
+ await CompileChecker.AssertNoErrors(generatedFiles);
+ });
}
diff --git a/TUnit.Assertions.SourceGenerator.Tests/CompileChecker.cs b/TUnit.Assertions.SourceGenerator.Tests/CompileChecker.cs
new file mode 100644
index 0000000000..9423b2084b
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/CompileChecker.cs
@@ -0,0 +1,43 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace TUnit.Assertions.SourceGenerator.Tests;
+
+///
+/// Parses generator output as C# and asserts there are no error-severity diagnostics.
+/// Pins the entire class of source-generator emit bugs (mis-paired brackets, wrong
+/// default-value rendering, invalid generic parameter lists) regardless of the specific
+/// diagnostic id. Use alongside content-shape assertions when the test wants both a
+/// targeted regression check and a structural compile-clean gate.
+///
+internal static class CompileChecker
+{
+ public static async Task AssertNoErrors(IEnumerable generatedFiles)
+ {
+ 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 files. 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", System.StringComparison.Ordinal))
+#endif
+ .Select(d => $"{d.Id}: {d.GetMessage()}")
+ .ToArray();
+
+ await Assert.That(errors).IsEmpty();
+ }
+}
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet10_0.verified.txt
new file mode 100644
index 0000000000..f83688fb8a
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet10_0.verified.txt
@@ -0,0 +1,64 @@
+[
+//
+#pragma warning disable
+#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 RespectsToken
+///
+public sealed class Int_RespectsToken_CancellationToken_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly System.Threading.CancellationToken _token;
+
+ public Int_RespectsToken_CancellationToken_Assertion(AssertionContext context, System.Threading.CancellationToken token)
+ : base(context)
+ {
+ _token = token;
+ }
+
+ 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}"));
+ }
+
+ var result = value!.RespectsToken(_token);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy RespectsToken({_token})";
+ }
+}
+
+public static partial class ValueTypeDefaultParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for RespectsToken
+ ///
+ public static Int_RespectsToken_CancellationToken_Assertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".RespectsToken({tokenExpression})");
+ return new Int_RespectsToken_CancellationToken_Assertion(source.Context, token);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet8_0.verified.txt
new file mode 100644
index 0000000000..f83688fb8a
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet8_0.verified.txt
@@ -0,0 +1,64 @@
+[
+//
+#pragma warning disable
+#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 RespectsToken
+///
+public sealed class Int_RespectsToken_CancellationToken_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly System.Threading.CancellationToken _token;
+
+ public Int_RespectsToken_CancellationToken_Assertion(AssertionContext context, System.Threading.CancellationToken token)
+ : base(context)
+ {
+ _token = token;
+ }
+
+ 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}"));
+ }
+
+ var result = value!.RespectsToken(_token);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy RespectsToken({_token})";
+ }
+}
+
+public static partial class ValueTypeDefaultParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for RespectsToken
+ ///
+ public static Int_RespectsToken_CancellationToken_Assertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".RespectsToken({tokenExpression})");
+ return new Int_RespectsToken_CancellationToken_Assertion(source.Context, token);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet9_0.verified.txt
new file mode 100644
index 0000000000..f83688fb8a
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.DotNet9_0.verified.txt
@@ -0,0 +1,64 @@
+[
+//
+#pragma warning disable
+#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 RespectsToken
+///
+public sealed class Int_RespectsToken_CancellationToken_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly System.Threading.CancellationToken _token;
+
+ public Int_RespectsToken_CancellationToken_Assertion(AssertionContext context, System.Threading.CancellationToken token)
+ : base(context)
+ {
+ _token = token;
+ }
+
+ 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}"));
+ }
+
+ var result = value!.RespectsToken(_token);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy RespectsToken({_token})";
+ }
+}
+
+public static partial class ValueTypeDefaultParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for RespectsToken
+ ///
+ public static Int_RespectsToken_CancellationToken_Assertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".RespectsToken({tokenExpression})");
+ return new Int_RespectsToken_CancellationToken_Assertion(source.Context, token);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.Net4_7.verified.txt
new file mode 100644
index 0000000000..f83688fb8a
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ValueTypeDefaultParameter.Net4_7.verified.txt
@@ -0,0 +1,64 @@
+[
+//
+#pragma warning disable
+#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 RespectsToken
+///
+public sealed class Int_RespectsToken_CancellationToken_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly System.Threading.CancellationToken _token;
+
+ public Int_RespectsToken_CancellationToken_Assertion(AssertionContext context, System.Threading.CancellationToken token)
+ : base(context)
+ {
+ _token = token;
+ }
+
+ 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}"));
+ }
+
+ var result = value!.RespectsToken(_token);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy RespectsToken({_token})";
+ }
+}
+
+public static partial class ValueTypeDefaultParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for RespectsToken
+ ///
+ public static Int_RespectsToken_CancellationToken_Assertion RespectsToken(this IAssertionSource source, System.Threading.CancellationToken token = default, [CallerArgumentExpression(nameof(token))] string? tokenExpression = null)
+ {
+ source.Context.ExpressionBuilder.Append($".RespectsToken({tokenExpression})");
+ return new Int_RespectsToken_CancellationToken_Assertion(source.Context, token);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
index 43fc0d2c60..06bd514fd0 100644
--- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
@@ -224,6 +224,29 @@ public Task MethodWithDefaultValues() => RunTest(
await Assert.That(mainFile).Contains("bool exact = true");
});
+ [Test]
+ public Task ValueTypeDefaultParameter() => RunTest(
+ Path.Combine(Sourcy.Git.RootDirectory.FullName,
+ "TUnit.Assertions.SourceGenerator.Tests",
+ "TestData",
+ "ValueTypeDefaultParameterAssertion.cs"),
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles).Count().IsEqualTo(1);
+
+ var mainFile = generatedFiles.First();
+ await Assert.That(mainFile).IsNotNull();
+
+ // A non-nullable value-type parameter declared with `= default` must render as
+ // the bare `default` literal, not `= null`. The literal `null` is invalid for a
+ // non-nullable value type and produces CS1750. The trailing comma anchors the
+ // assertion to bare `default`, ruling out the longer `default(TypeName)` form.
+ await Assert.That(mainFile).Contains("CancellationToken token = default,");
+ await Assert.That(mainFile).DoesNotContain("CancellationToken token = null");
+
+ await CompileChecker.AssertNoErrors(generatedFiles);
+ });
+
#if NET8_0_OR_GREATER
[Test]
public Task RefStructParameter() => RunTest(
diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/AssertionExtensionValueTypeDefaultParameterAssertion.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/AssertionExtensionValueTypeDefaultParameterAssertion.cs
new file mode 100644
index 0000000000..b70aa905b6
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/AssertionExtensionValueTypeDefaultParameterAssertion.cs
@@ -0,0 +1,33 @@
+using System.Threading;
+using TUnit.Assertions.Attributes;
+using TUnit.Assertions.Core;
+
+namespace TUnit.Assertions.Tests.TestData;
+
+///
+/// Test case: -decorated class whose constructor
+/// has a non-nullable value-type parameter declared with = default. The Roslyn-reported
+/// default expression is , but emitting parameter = null for a
+/// value type is invalid C# (CS1750). The generator must render the bare default
+/// literal, which the C# compiler infers as default(TypeName) from the parameter type.
+///
+[AssertionExtension("RespectsToken")]
+public class StringRespectsTokenAssertion : Assertion
+{
+ private readonly CancellationToken _token;
+
+ public StringRespectsTokenAssertion(AssertionContext context, CancellationToken token = default)
+ : base(context)
+ {
+ _token = token;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ return _token.IsCancellationRequested
+ ? Task.FromResult(AssertionResult.Failed("token was canceled"))
+ : Task.FromResult(AssertionResult.Passed);
+ }
+
+ protected override string GetExpectation() => "to respect the supplied token";
+}
diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/ValueTypeDefaultParameterAssertion.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/ValueTypeDefaultParameterAssertion.cs
new file mode 100644
index 0000000000..390c67ad81
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/ValueTypeDefaultParameterAssertion.cs
@@ -0,0 +1,20 @@
+using System.Threading;
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Tests.TestData;
+
+///
+/// Test case: -decorated method whose parameter
+/// is a non-nullable value type declared with = default. The Roslyn-reported default
+/// expression is , but emitting parameter = null for a value
+/// type is invalid C# (CS1750). The generator must render the bare default literal,
+/// which the C# compiler infers as default(TypeName) from the parameter type.
+///
+public static partial class ValueTypeDefaultParameterAssertionExtensions
+{
+ [GenerateAssertion]
+ public static bool RespectsToken(this int value, CancellationToken token = default)
+ {
+ return !token.IsCancellationRequested && value > 0;
+ }
+}
diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
index a3f3fed290..d87b8b4993 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
@@ -411,7 +411,7 @@ private static void GenerateExtensionMethod(
// Add default value if present
if (param.HasExplicitDefaultValue)
{
- var defaultValue = FormatDefaultValue(param.ExplicitDefaultValue, param.Type);
+ var defaultValue = DefaultValueFormatter.FormatDefaultValue(param.ExplicitDefaultValue, param.Type);
sourceBuilder.Append($" = {defaultValue}");
}
}
@@ -496,48 +496,6 @@ private static void GenerateExtensionMethod(
sourceBuilder.AppendLine(" }");
}
- private static string FormatDefaultValue(object? defaultValue, ITypeSymbol type)
- {
- if (defaultValue == null)
- {
- return "null";
- }
-
- if (type.TypeKind == TypeKind.Enum && type is INamedTypeSymbol enumType)
- {
- // Find the enum member that matches the default value
- foreach (var member in enumType.GetMembers())
- {
- if (member is IFieldSymbol { HasConstantValue: true } field &&
- field.ConstantValue != null &&
- field.ConstantValue.Equals(defaultValue))
- {
- // Use just the enum name without namespace since we have using TUnit.Assertions.Enums;
- return $"{enumType.Name}.{field.Name}";
- }
- }
- // Fallback if no matching member found
- 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";
- }
-
private record AssertionExtensionData(
INamedTypeSymbol ClassSymbol,
string MethodName,
diff --git a/TUnit.Assertions.SourceGenerator/Generators/DefaultValueFormatter.cs b/TUnit.Assertions.SourceGenerator/Generators/DefaultValueFormatter.cs
new file mode 100644
index 0000000000..c62a989974
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator/Generators/DefaultValueFormatter.cs
@@ -0,0 +1,93 @@
+using Microsoft.CodeAnalysis;
+
+namespace TUnit.Assertions.SourceGenerator.Generators;
+
+///
+/// Shared default-value emit for both assertion source generators. Renders a parameter's
+/// Roslyn-reported default value as the C# literal that goes after = in the generated
+/// method signature. The two public entry points differ only in how they format enum members:
+/// emits the short form (Enum.Member), suitable for
+/// generators that rely on using directives at the top of the generated file;
+/// emits the namespace-prefixed form
+/// (Namespace.Enum.Member), suitable for generators that emit fully-qualified type
+/// names throughout the produced source.
+///
+internal static class DefaultValueFormatter
+{
+ ///
+ /// Formats as a C# default-literal expression for a
+ /// parameter of , emitting enum members in the short form
+ /// (Enum.Member) that depends on a using directive to resolve the type name.
+ ///
+ /// The constant value Roslyn reported for the parameter's
+ /// explicit default, or if the parameter was declared with the
+ /// default keyword (which Roslyn surfaces as a null constant).
+ /// The parameter's declared type.
+ public static string FormatDefaultValue(object? defaultValue, ITypeSymbol type)
+ => FormatDefaultValueCore(defaultValue, type, useFullyQualifiedEnumName: false);
+
+ ///
+ /// Formats as a C# default-literal expression for a
+ /// parameter of , emitting enum members in the fully-qualified form
+ /// (Namespace.Enum.Member) so the result resolves regardless of using
+ /// directives at the top of the generated file.
+ ///
+ /// The constant value Roslyn reported for the parameter's
+ /// explicit default, or if the parameter was declared with the
+ /// default keyword (which Roslyn surfaces as a null constant).
+ /// The parameter's declared type.
+ public static string FormatDefaultValueFullyQualified(object? defaultValue, ITypeSymbol type)
+ => FormatDefaultValueCore(defaultValue, type, useFullyQualifiedEnumName: true);
+
+ private static string FormatDefaultValueCore(object? defaultValue, ITypeSymbol type, bool useFullyQualifiedEnumName)
+ {
+ if (defaultValue == null)
+ {
+ // A null Roslyn-reported default expression on a non-nullable value type means the
+ // parameter was declared with `= default` (e.g. `CancellationToken ct = default`).
+ // Emitting `= null` for such a parameter produces CS1750 because the literal null
+ // cannot convert to the value-type. Emit the `default` literal: the target type is
+ // inferred from the parameter, matching the user's original declaration.
+ // Nullable stays on the null path: it accepts a literal null default.
+ if (type.IsValueType
+ && type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T)
+ {
+ return "default";
+ }
+ return "null";
+ }
+
+ if (type.TypeKind == TypeKind.Enum && type is INamedTypeSymbol enumType)
+ {
+ var enumPrefix = useFullyQualifiedEnumName ? enumType.ToDisplayString() : enumType.Name;
+ foreach (var member in enumType.GetMembers())
+ {
+ if (member is IFieldSymbol { HasConstantValue: true } field &&
+ field.ConstantValue != null &&
+ field.ConstantValue.Equals(defaultValue))
+ {
+ return $"{enumPrefix}.{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";
+ }
+}
diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
index 3d3db7de51..fe674ec808 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
@@ -141,7 +141,7 @@ private static (AssertionMethodData? Data, Diagnostic? Diagnostic) GetAssertionM
IsInterpolatedStringHandler = IsInterpolatedStringHandler(p.Type),
SimpleTypeName = GetSimpleTypeName(p.Type),
HasExplicitDefaultValue = p.HasExplicitDefaultValue,
- DefaultValueExpression = p.HasExplicitDefaultValue ? FormatDefaultValue(p.ExplicitDefaultValue, p.Type) : null,
+ DefaultValueExpression = p.HasExplicitDefaultValue ? DefaultValueFormatter.FormatDefaultValueFullyQualified(p.ExplicitDefaultValue, p.Type) : null,
}).ToImmutableEquatableArray();
// Extract custom expectation message and inlining preference if provided
@@ -1199,46 +1199,6 @@ 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"