diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
index 5f975588103..565893474b6 100644
--- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
@@ -201,4 +201,37 @@ public Task FileScopedClassWithInlining() => RunTest(
// Snapshot test - the actual verification is done by snapshot comparison
await Assert.That(generatedFiles.Count).IsGreaterThanOrEqualTo(1);
});
+
+#if NET6_0_OR_GREATER
+ [Test]
+ public Task RefStructParameter() => RunTest(
+ Path.Combine(Sourcy.Git.RootDirectory.FullName,
+ "TUnit.Assertions.SourceGenerator.Tests",
+ "TestData",
+ "RefStructParameterAssertion.cs"),
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles).HasCount(1);
+
+ var mainFile = generatedFiles.First();
+ await Assert.That(mainFile).IsNotNull();
+
+ // Verify that the field type is string, not the ref struct
+ await Assert.That(mainFile).Contains("private readonly string _message;");
+ await Assert.That(mainFile).Contains("private readonly string _suffix;");
+
+ // Verify that the extension method converts the ref struct to string
+ await Assert.That(mainFile).Contains("message.ToStringAndClear()");
+ await Assert.That(mainFile).Contains("suffix.ToStringAndClear()");
+
+ // Verify the constructor takes string, not the ref struct
+ await Assert.That(mainFile).Contains("string message)");
+ await Assert.That(mainFile).Contains("string suffix)");
+
+ // Verify that .ToStringAndClear() is removed in the inlined body
+ // (since the field is already a string)
+ // The inlined body should use _message directly, not _message.ToStringAndClear()
+ await Assert.That(mainFile).Contains("value!.Contains(_message)");
+ });
+#endif
}
diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/RefStructParameterAssertion.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/RefStructParameterAssertion.cs
new file mode 100644
index 00000000000..6fa3559fa05
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/RefStructParameterAssertion.cs
@@ -0,0 +1,30 @@
+#if NET6_0_OR_GREATER
+using System.Runtime.CompilerServices;
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Tests.TestData;
+
+///
+/// Test case: Method with ref struct parameter (DefaultInterpolatedStringHandler)
+/// The generator should convert the ref struct to string before storing it
+///
+public static class RefStructParameterAssertions
+{
+ ///
+ /// Test that interpolated string handlers are properly converted to strings
+ ///
+ [GenerateAssertion(ExpectationMessage = "to contain {message}", InlineMethodBody = true)]
+ public static bool ContainsMessage(this string value, ref DefaultInterpolatedStringHandler message)
+ {
+ var stringMessage = message.ToStringAndClear();
+ return value.Contains(stringMessage);
+ }
+
+ ///
+ /// Test with a simpler expression body
+ ///
+ [GenerateAssertion(ExpectationMessage = "to end with {suffix}", InlineMethodBody = true)]
+ public static bool EndsWithMessage(this string value, ref DefaultInterpolatedStringHandler suffix)
+ => value.EndsWith(suffix.ToStringAndClear());
+}
+#endif
diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
index 177cc51d831..95bbfcae85f 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
@@ -47,6 +47,15 @@ public sealed class MethodAssertionGenerator : IIncrementalGenerator
isEnabledByDefault: true,
description: "Methods decorated with [GenerateAssertion] must return bool, AssertionResult, Task, or Task.");
+ private static readonly DiagnosticDescriptor RefStructRequiresInliningRule = new DiagnosticDescriptor(
+ id: "TUNITGEN004",
+ title: "Ref struct parameter requires method body inlining",
+ messageFormat: "Method '{0}' has ref struct parameter '{1}' of type '{2}'. Use InlineMethodBody = true and ensure the method has a single-expression or single-return-statement body",
+ category: "TUnit.Assertions.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Methods with ref struct parameters (like DefaultInterpolatedStringHandler) require InlineMethodBody = true because ref structs cannot be stored as class fields. The method must have a simple body that can be inlined.");
+
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all methods decorated with [GenerateAssertion]
@@ -259,6 +268,22 @@ private static (AssertionMethodData? Data, Diagnostic? Diagnostic) GetAssertionM
}
}
+ // Validate that methods with ref struct parameters have inlined method bodies
+ // Ref structs cannot be stored as class fields, so we need to inline the method body
+ foreach (var param in additionalParameters)
+ {
+ if (IsRefStruct(param.Type) && string.IsNullOrEmpty(methodBody))
+ {
+ var diagnostic = Diagnostic.Create(
+ RefStructRequiresInliningRule,
+ location,
+ methodSymbol.Name,
+ param.Name,
+ param.Type.ToDisplayString());
+ return (null, diagnostic);
+ }
+ }
+
var data = new AssertionMethodData(
methodSymbol,
targetType,
@@ -545,9 +570,11 @@ private static void GenerateAssertionClass(StringBuilder sb, AssertionMethodData
sb.AppendLine("{");
// Private fields for additional parameters
+ // Note: Ref struct types (like DefaultInterpolatedStringHandler) are stored as string
foreach (var param in data.AdditionalParameters)
{
- sb.AppendLine($" private readonly {param.Type.ToDisplayString()} _{param.Name};");
+ var fieldType = IsRefStruct(param.Type) ? "string" : param.Type.ToDisplayString();
+ sb.AppendLine($" private readonly {fieldType} _{param.Name};");
}
if (data.AdditionalParameters.Length > 0)
@@ -556,10 +583,12 @@ private static void GenerateAssertionClass(StringBuilder sb, AssertionMethodData
}
// Constructor
+ // Note: Ref struct parameters are received as string (pre-converted by extension method)
sb.Append($" public {className}(AssertionContext<{targetTypeName}> context");
foreach (var param in data.AdditionalParameters)
{
- sb.Append($", {param.Type.ToDisplayString()} {param.Name}");
+ var paramType = IsRefStruct(param.Type) ? "string" : param.Type.ToDisplayString();
+ sb.Append($", {paramType} {param.Name}");
}
sb.AppendLine(")");
sb.AppendLine(" : base(context)");
@@ -730,6 +759,26 @@ private static string BuildInlinedExpression(AssertionMethodData data)
$"_{paramName}");
}
+ // For ref struct parameters that have been converted to strings,
+ // remove calls to .ToStringAndClear() and .ToString() since the value is already a string
+ foreach (var param in data.AdditionalParameters)
+ {
+ if (IsRefStruct(param.Type))
+ {
+ var fieldName = $"_{param.Name}";
+ // Remove .ToStringAndClear() - the value is already a string
+ inlinedBody = Regex.Replace(
+ inlinedBody,
+ $@"{Regex.Escape(fieldName)}\.ToStringAndClear\(\)",
+ fieldName);
+ // Remove .ToString() - the value is already a string
+ inlinedBody = Regex.Replace(
+ inlinedBody,
+ $@"{Regex.Escape(fieldName)}\.ToString\(\)",
+ fieldName);
+ }
+ }
+
// Add null-forgiving operator for reference types if not already present
// This is safe because we've already checked for null above
var isNullable = data.TargetType.IsReferenceType || data.TargetType.NullableAnnotation == NullableAnnotation.Annotated;
@@ -873,10 +922,23 @@ private static void GenerateExtensionMethod(StringBuilder sb, AssertionMethodDat
}
// Construct and return assertion
+ // Note: Ref struct parameters (like interpolated string handlers) are converted to string
sb.Append($" return new {className}{genericDeclaration}(source.Context");
foreach (var param in data.AdditionalParameters)
{
- sb.Append($", {param.Name}");
+ if (IsRefStruct(param.Type))
+ {
+ // Convert ref struct to string - use ToStringAndClear for interpolated string handlers
+ // or ToString() for other ref structs
+ var conversion = IsInterpolatedStringHandler(param.Type)
+ ? $"{param.Name}.ToStringAndClear()"
+ : $"{param.Name}.ToString()";
+ sb.Append($", {conversion}");
+ }
+ else
+ {
+ sb.Append($", {param.Name}");
+ }
}
sb.AppendLine(");");
@@ -1001,6 +1063,61 @@ private static List CollectGenericConstraints(IMethodSymbol method)
return constraints;
}
+ ///
+ /// Checks if a type is a ref struct (ref-like type).
+ /// Ref structs cannot be stored as fields in classes.
+ ///
+ private static bool IsRefStruct(ITypeSymbol type)
+ {
+ if (type is not INamedTypeSymbol namedType)
+ {
+ return false;
+ }
+
+ // Use reflection to access IsRefLikeType property which may not be available in all Roslyn versions
+ var isRefLikeProperty = namedType.GetType().GetProperty("IsRefLikeType");
+ if (isRefLikeProperty?.GetValue(namedType) is bool isRefLike && isRefLike)
+ {
+ return true;
+ }
+
+ // Fallback: check for common ref struct types by name
+ var typeName = namedType.ToDisplayString();
+ if (typeName.StartsWith("System.Span<") ||
+ typeName.StartsWith("System.ReadOnlySpan<") ||
+ typeName == "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler")
+ {
+ return true;
+ }
+
+ // Check for InterpolatedStringHandlerAttribute on the type
+ return namedType.GetAttributes().Any(attr =>
+ attr.AttributeClass?.ToDisplayString() == "System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute");
+ }
+
+ ///
+ /// Checks if a type is an interpolated string handler (e.g., DefaultInterpolatedStringHandler).
+ /// These types need special handling as they should be converted to string.
+ ///
+ private static bool IsInterpolatedStringHandler(ITypeSymbol type)
+ {
+ if (type is not INamedTypeSymbol namedType)
+ {
+ return false;
+ }
+
+ // Check for DefaultInterpolatedStringHandler specifically
+ var typeName = namedType.ToDisplayString();
+ if (typeName == "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler")
+ {
+ return true;
+ }
+
+ // Check for InterpolatedStringHandlerAttribute on the type
+ return namedType.GetAttributes().Any(attr =>
+ attr.AttributeClass?.ToDisplayString() == "System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute");
+ }
+
private enum ReturnTypeKind
{
Bool,