Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
});
Comment on lines +219 to +235
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertions verify the generated code contains specific strings, but these are quite brittle and might break if formatting or whitespace changes. Additionally, line 234 checks for value!.Contains(_message) which assumes specific formatting.

Consider using snapshot testing (like the FileScopedClassWithInlining test on line 194) instead of string-based assertions. This would make the tests more robust and easier to maintain, and you'd be able to review the full generated output in the snapshot files.

Copilot uses AI. Check for mistakes.
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#if NET6_0_OR_GREATER
using System.Runtime.CompilerServices;
using TUnit.Assertions.Attributes;

namespace TUnit.Assertions.Tests.TestData;

/// <summary>
/// Test case: Method with ref struct parameter (DefaultInterpolatedStringHandler)
/// The generator should convert the ref struct to string before storing it
/// </summary>
public static class RefStructParameterAssertions
{
/// <summary>
/// Test that interpolated string handlers are properly converted to strings
/// </summary>
[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);
}

/// <summary>
/// Test with a simpler expression body
/// </summary>
[GenerateAssertion(ExpectationMessage = "to end with {suffix}", InlineMethodBody = true)]
public static bool EndsWithMessage(this string value, ref DefaultInterpolatedStringHandler suffix)
=> value.EndsWith(suffix.ToStringAndClear());
Comment on lines +16 to +28
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test methods use ref DefaultInterpolatedStringHandler parameters and call .ToStringAndClear() within the method bodies. However, this pattern might not accurately represent real-world usage of interpolated string handlers.

Typically, DefaultInterpolatedStringHandler parameters are used with the C# compiler's interpolated string syntax (e.g., $"string {value}"), where the compiler automatically creates and manages the handler. Manually passing a ref DefaultInterpolatedStringHandler is uncommon.

Consider adding a test case that shows how this feature would actually be used in practice, or document why users would want to create assertions with ref struct parameters. This would help clarify the intended use case and validate that the implementation supports real-world scenarios.

Copilot uses AI. Check for mistakes.
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
public sealed class MethodAssertionGenerator : IIncrementalGenerator
{
private static readonly DiagnosticDescriptor MethodMustBeStaticRule = new DiagnosticDescriptor(
id: "TUNITGEN001",

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Włącz śledzenie wydań analizatora dla projektu analizatora zawierającego regułę „TUNITGEN001” (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Analyseversionsnachverfolgung für Analyseprojekt mit Regel "TUNITGEN001" aktivieren (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 24 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
title: "Method must be static",
messageFormat: "Method '{0}' decorated with [GenerateAssertion] must be static",
category: "TUnit.Assertions.SourceGenerator",
Expand All @@ -30,7 +30,7 @@
description: "Methods decorated with [GenerateAssertion] must be static to be used in generated assertions.");

private static readonly DiagnosticDescriptor MethodMustHaveParametersRule = new DiagnosticDescriptor(
id: "TUNITGEN002",

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Activer le suivi de version d'analyseur pour le projet d'analyseur contenant la règle 'TUNITGEN002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Włącz śledzenie wydań analizatora dla projektu analizatora zawierającego regułę „TUNITGEN002” (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Analyseversionsnachverfolgung für Analyseprojekt mit Regel "TUNITGEN002" aktivieren (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 33 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
title: "Method must have at least one parameter",
messageFormat: "Method '{0}' decorated with [GenerateAssertion] must have at least one parameter (the value to assert)",
category: "TUnit.Assertions.SourceGenerator",
Expand All @@ -39,14 +39,23 @@
description: "Methods decorated with [GenerateAssertion] must have at least one parameter representing the value being asserted.");

private static readonly DiagnosticDescriptor UnsupportedReturnTypeRule = new DiagnosticDescriptor(
id: "TUNITGEN003",

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Activer le suivi de version d'analyseur pour le projet d'analyseur contenant la règle 'TUNITGEN003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Włącz śledzenie wydań analizatora dla projektu analizatora zawierającego regułę „TUNITGEN003” (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Analyseversionsnachverfolgung für Analyseprojekt mit Regel "TUNITGEN003" aktivieren (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 42 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
title: "Unsupported return type",
messageFormat: "Method '{0}' decorated with [GenerateAssertion] has unsupported return type '{1}'. Supported types are: bool, AssertionResult, Task<bool>, Task<AssertionResult>",

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Le message du diagnostic ne doit comporter aucun caractère de retour de ligne et aucun espace blanc de début ou de fin, et doit tenir en une seule phrase sans point final ou en plusieurs phrases avec un point final

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Komunikat dotyczący diagnostyki nie powinien zawierać znaku nowego wiersza ani odstępów na początku i końcu oraz powinien być pojedynczym zdaniem bez kropki na końcu lub wieloma zdaniami z kropkami na końcu

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Die Diagnosemeldung darf keine Zeilenvorschubzeichen und keine führenden oder nachfolgenden Leerzeichen enthalten und muss entweder einen einzelnen Satz ohne Satzendepunkt oder mehrere Sätze mit Satzendepunkt umfassen.

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 44 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period
category: "TUnit.Assertions.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Methods decorated with [GenerateAssertion] must return bool, AssertionResult, Task<bool>, or Task<AssertionResult>.");

private static readonly DiagnosticDescriptor RefStructRequiresInliningRule = new DiagnosticDescriptor(
id: "TUNITGEN004",

Check warning on line 51 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN004' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 51 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN004' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)

Check warning on line 51 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Enable analyzer release tracking for the analyzer project containing rule 'TUNITGEN004' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
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",

Check warning on line 53 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Le message du diagnostic ne doit comporter aucun caractère de retour de ligne et aucun espace blanc de début ou de fin, et doit tenir en une seule phrase sans point final ou en plusieurs phrases avec un point final

Check warning on line 53 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Die Diagnosemeldung darf keine Zeilenvorschubzeichen und keine führenden oder nachfolgenden Leerzeichen enthalten und muss entweder einen einzelnen Satz ohne Satzendepunkt oder mehrere Sätze mit Satzendepunkt umfassen.

Check warning on line 53 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 53 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 53 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period

Check warning on line 53 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

The diagnostic message should not contain any line return character nor any leading or trailing whitespaces and should either be a single sentence without a trailing period or a multi-sentences with a trailing period
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]
Expand Down Expand Up @@ -259,6 +268,22 @@
}
}

// 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());
Comment on lines +272 to +282
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message for TUNITGEN004 states "Use InlineMethodBody = true and ensure the method has a single-expression or single-return-statement body" but the code only checks if methodBody is empty or not. It doesn't validate that when InlineMethodBody = true is specified, the method actually has a valid body that can be inlined.

A user could set InlineMethodBody = true on a method with a complex multi-statement body, pass the validation at line 275, but then get unexpected behavior because the body extraction logic (lines 247-269) would fail to extract a method body from complex methods, leaving methodBody as null. This would silently fail to inline.

Consider adding validation to ensure that when ref struct parameters are present, the method body was successfully extracted and is not null/empty even when InlineMethodBody = true is specified.

Suggested change
// 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());
// Ref structs cannot be stored as class fields, so we need to inline the method body.
// The body must be either an expression-bodied member or a single return statement with an expression.
var firstRefStructParam = additionalParameters.FirstOrDefault(p => IsRefStruct(p.Type));
if (firstRefStructParam is not null)
{
var hasInlinableBody =
methodSyntax.ExpressionBody is not null ||
(methodSyntax.Body is not null &&
methodSyntax.Body.Statements.Count == 1 &&
methodSyntax.Body.Statements[0] is ReturnStatementSyntax { Expression: not null });
if (!hasInlinableBody)
{
var diagnostic = Diagnostic.Create(
RefStructRequiresInliningRule,
location,
methodSymbol.Name,
firstRefStructParam.Name,
firstRefStructParam.Type.ToDisplayString());

Copilot uses AI. Check for mistakes.
return (null, diagnostic);
}
}

var data = new AssertionMethodData(
methodSymbol,
targetType,
Expand Down Expand Up @@ -545,9 +570,11 @@
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)
Expand All @@ -556,10 +583,12 @@
}

// 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)");
Expand Down Expand Up @@ -730,10 +759,30 @@
$"_{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\(\)",
Comment on lines +772 to +777
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex replacement logic to remove .ToStringAndClear() and .ToString() calls may be too broad and could accidentally match and remove these method calls on other objects that happen to have the same field name prefix.

For example, if you have code like var otherObj_message = GetMessage(); return value.Contains(otherObj_message.ToStringAndClear()), the regex would incorrectly remove the .ToStringAndClear() call even though otherObj_message is not the ref struct field.

Consider adding word boundary anchors or being more specific in the pattern to ensure you're only replacing method calls on the exact field name, not on other variables that happen to start with the same name.

Suggested change
$@"{Regex.Escape(fieldName)}\.ToStringAndClear\(\)",
fieldName);
// Remove .ToString() - the value is already a string
inlinedBody = Regex.Replace(
inlinedBody,
$@"{Regex.Escape(fieldName)}\.ToString\(\)",
$@"\b{Regex.Escape(fieldName)}\.ToStringAndClear\(\)",
fieldName);
// Remove .ToString() - the value is already a string
inlinedBody = Regex.Replace(
inlinedBody,
$@"\b{Regex.Escape(fieldName)}\.ToString\(\)",

Copilot uses AI. Check for mistakes.
fieldName);
}
}
Comment on lines +764 to +780
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.

// 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;
if (isNullable && !string.IsNullOrEmpty(inlinedBody) && !inlinedBody.StartsWith("value!"))

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Déréférencement d'une éventuelle référence null.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Wyłuskanie odwołania, które może mieć wartość null.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Dereferenzierung eines möglichen Nullverweises.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Dereference of a possibly null reference.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Dereference of a possibly null reference.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Dereference of a possibly null reference.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Dereference of a possibly null reference.

Check warning on line 785 in TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Dereference of a possibly null reference.
{
// Replace null-conditional operators with null-forgiving + regular operators
// value?.Member becomes value!.Member (safe because we already null-checked)
Expand Down Expand Up @@ -873,10 +922,23 @@
}

// 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}");
Comment on lines +929 to +936
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The choice between .ToStringAndClear() and .ToString() for ref structs is based solely on whether the type is an interpolated string handler. However, for ref parameters of interpolated string handlers, calling .ToStringAndClear() may not be semantically correct because it mutates the original value (clearing it), which might be unexpected when using a ref parameter.

If the original method expects to receive a ref DefaultInterpolatedStringHandler and potentially use it multiple times or pass it to other methods, clearing it at the extension method boundary could break the expected behavior.

Consider using .ToString() instead of .ToStringAndClear() for ref parameters, even for interpolated string handlers, to avoid unexpected mutations. Alternatively, document this behavior clearly so users understand that ref struct parameters passed to generated assertions will be consumed/cleared.

Copilot uses AI. Check for mistakes.
}
else
{
sb.Append($", {param.Name}");
}
}
sb.AppendLine(");");

Expand Down Expand Up @@ -1001,6 +1063,61 @@
return constraints;
}

/// <summary>
/// Checks if a type is a ref struct (ref-like type).
/// Ref structs cannot be stored as fields in classes.
/// </summary>
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;
}
Comment on lines +1077 to +1082
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses runtime reflection (namedType.GetType().GetProperty("IsRefLikeType")) to detect the IsRefLikeType property because it may not be available in all Roslyn versions. However, this approach has a performance cost since it uses reflection on every call.

Consider caching the PropertyInfo or the result of checking if the property exists. Since this method will be called for every parameter in every method being processed, the repeated reflection lookups could add up. You could cache the PropertyInfo in a static field or check once if the property is available on the current Roslyn version.

Copilot uses AI. Check for mistakes.

// 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
Comment on lines +1077 to +1093
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic for detecting ref structs using string name comparison is incomplete. It only checks for Span<T>, ReadOnlySpan<T>, and DefaultInterpolatedStringHandler, but there are other common ref struct types in .NET that would be missed, such as:

  • System.ReadOnlySpan (non-generic)
  • System.Span (non-generic)
  • System.Runtime.InteropServices.ArraySegment<T> variants
  • System.Threading.Tasks.ValueTask<T> (not a ref struct, but worth noting)
  • Custom user-defined ref structs

While the InterpolatedStringHandlerAttribute check at lines 1094-1095 helps catch custom interpolated string handlers, it won't catch other custom ref structs that don't use that attribute.

Consider documenting this limitation or expanding the fallback checks. Alternatively, since you're targeting modern Roslyn versions (the project appears to be .NET 6+), you might want to verify if IsRefLikeType is always available and remove the fallback entirely.

Suggested change
// 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
// Prefer Roslyn's built-in ref-like detection. This correctly identifies all ref struct types,
// including user-defined ones, on the modern Roslyn versions this project targets.
if (namedType.IsRefLikeType)
{
return true;
}
// As a very narrow fallback (e.g., for custom interpolated string handlers),
// treat types marked with InterpolatedStringHandlerAttribute as ref structs.

Copilot uses AI. Check for mistakes.
return namedType.GetAttributes().Any(attr =>
attr.AttributeClass?.ToDisplayString() == "System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute");
}

/// <summary>
/// Checks if a type is an interpolated string handler (e.g., DefaultInterpolatedStringHandler).
/// These types need special handling as they should be converted to string.
/// </summary>
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");
}
Comment on lines +1066 to +1119
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both IsRefStruct and IsInterpolatedStringHandler methods duplicate the same attribute checking logic. The check for InterpolatedStringHandlerAttribute appears in both methods (lines 1094-1095 and 1117-1118).

Consider extracting this common logic into a helper method, or have IsInterpolatedStringHandler call IsRefStruct first to check if it's a ref struct, then add the additional specific checks for interpolated string handlers. This would reduce code duplication and make maintenance easier.

Copilot uses AI. Check for mistakes.

private enum ReturnTypeKind
{
Bool,
Expand Down
Loading