diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet10_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet10_0.verified.txt
new file mode 100644
index 0000000000..707c766c63
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet10_0.verified.txt
@@ -0,0 +1,328 @@
+[
+//
+#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 ContainsAny
+///
+public sealed class String_ContainsAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _label;
+ private readonly string[] _candidates;
+
+ public String_ContainsAny_String_StringArray_Assertion(AssertionContext context, string label, string[] candidates)
+ : base(context)
+ {
+ _label = label;
+ _candidates = candidates;
+ }
+
+ 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!.ContainsAny(_label, _candidates);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsAny({_label}, {_candidates})";
+ }
+}
+
+///
+/// Generated assertion for IsBetweenExcluding
+///
+public sealed class Int_IsBetweenExcluding_Int_Int_IntArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _min;
+ private readonly int _max;
+ private readonly int[] _excluded;
+
+ public Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(AssertionContext context, int min, int max, int[] excluded)
+ : base(context)
+ {
+ _min = min;
+ _max = max;
+ _excluded = excluded;
+ }
+
+ 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!.IsBetweenExcluding(_min, _max, _excluded);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsBetweenExcluding({_min}, {_max}, {_excluded})";
+ }
+}
+
+///
+/// Generated assertion for MeetsLength
+///
+public sealed class String_MeetsLength_Int_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _minLength;
+ private readonly string[] _suffixes;
+
+ public String_MeetsLength_Int_StringArray_Assertion(AssertionContext context, int minLength, string[] suffixes)
+ : base(context)
+ {
+ _minLength = minLength;
+ _suffixes = suffixes;
+ }
+
+ 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!.MeetsLength(_minLength, _suffixes);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy MeetsLength({_minLength}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for IsOneOfWithDefault
+///
+[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+public sealed class T_IsOneOfWithDefault_T_TArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly T _fallback;
+ private readonly T[] _alternatives;
+
+ public T_IsOneOfWithDefault_T_TArray_Assertion(AssertionContext context, T fallback, T[] alternatives)
+ : base(context)
+ {
+ _fallback = fallback;
+ _alternatives = alternatives;
+ }
+
+ 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!.IsOneOfWithDefault(_fallback, _alternatives);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsOneOfWithDefault({_fallback}, {_alternatives})";
+ }
+}
+
+///
+/// Generated assertion for StartsWithAny
+///
+public sealed class String_StartsWithAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _prefix;
+ private readonly string[] _suffixes;
+
+ public String_StartsWithAny_String_StringArray_Assertion(AssertionContext context, string prefix, string[] suffixes)
+ : base(context)
+ {
+ _prefix = prefix;
+ _suffixes = suffixes;
+ }
+
+ 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 = _suffixes.Length >= 0 && value!.StartsWith(_prefix);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy StartsWithAny({_prefix}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for ContainsExactly
+///
+public sealed class String_ContainsExactly_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string[] _required;
+
+ public String_ContainsExactly_StringArray_Assertion(AssertionContext context, string[] required)
+ : base(context)
+ {
+ _required = required;
+ }
+
+ 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!.ContainsExactly(_required);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsExactly({_required})";
+ }
+}
+
+public static partial class ParamsParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for ContainsAny
+ ///
+ public static String_ContainsAny_String_StringArray_Assertion ContainsAny(this IAssertionSource source, string label, [CallerArgumentExpression(nameof(label))] string? labelExpression = null, params string[] candidates)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsAny({labelExpression}, {candidates})");
+ return new String_ContainsAny_String_StringArray_Assertion(source.Context, label, candidates);
+ }
+
+ ///
+ /// Generated extension method for IsBetweenExcluding
+ ///
+ public static Int_IsBetweenExcluding_Int_Int_IntArray_Assertion IsBetweenExcluding(this IAssertionSource source, int min, int max, [CallerArgumentExpression(nameof(min))] string? minExpression = null, [CallerArgumentExpression(nameof(max))] string? maxExpression = null, params int[] excluded)
+ {
+ source.Context.ExpressionBuilder.Append($".IsBetweenExcluding({minExpression}, {maxExpression}, {excluded})");
+ return new Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(source.Context, min, max, excluded);
+ }
+
+ ///
+ /// Generated extension method for MeetsLength
+ ///
+ public static String_MeetsLength_Int_StringArray_Assertion MeetsLength(this IAssertionSource source, int minLength = 1, [CallerArgumentExpression(nameof(minLength))] string? minLengthExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".MeetsLength({minLengthExpression}, {suffixes})");
+ return new String_MeetsLength_Int_StringArray_Assertion(source.Context, minLength, suffixes);
+ }
+
+ ///
+ /// Generated extension method for IsOneOfWithDefault
+ ///
+ [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+ public static T_IsOneOfWithDefault_T_TArray_Assertion IsOneOfWithDefault(this IAssertionSource source, T fallback, [CallerArgumentExpression(nameof(fallback))] string? fallbackExpression = null, params T[] alternatives)
+ {
+ source.Context.ExpressionBuilder.Append($".IsOneOfWithDefault({fallbackExpression}, {alternatives})");
+ return new T_IsOneOfWithDefault_T_TArray_Assertion(source.Context, fallback, alternatives);
+ }
+
+ ///
+ /// Generated extension method for StartsWithAny
+ ///
+ public static String_StartsWithAny_String_StringArray_Assertion StartsWithAny(this IAssertionSource source, string prefix, [CallerArgumentExpression(nameof(prefix))] string? prefixExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".StartsWithAny({prefixExpression}, {suffixes})");
+ return new String_StartsWithAny_String_StringArray_Assertion(source.Context, prefix, suffixes);
+ }
+
+ ///
+ /// Generated extension method for ContainsExactly
+ ///
+ public static String_ContainsExactly_StringArray_Assertion ContainsExactly(this IAssertionSource source, params string[] required)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsExactly({required})");
+ return new String_ContainsExactly_StringArray_Assertion(source.Context, required);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet8_0.verified.txt
new file mode 100644
index 0000000000..707c766c63
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet8_0.verified.txt
@@ -0,0 +1,328 @@
+[
+//
+#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 ContainsAny
+///
+public sealed class String_ContainsAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _label;
+ private readonly string[] _candidates;
+
+ public String_ContainsAny_String_StringArray_Assertion(AssertionContext context, string label, string[] candidates)
+ : base(context)
+ {
+ _label = label;
+ _candidates = candidates;
+ }
+
+ 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!.ContainsAny(_label, _candidates);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsAny({_label}, {_candidates})";
+ }
+}
+
+///
+/// Generated assertion for IsBetweenExcluding
+///
+public sealed class Int_IsBetweenExcluding_Int_Int_IntArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _min;
+ private readonly int _max;
+ private readonly int[] _excluded;
+
+ public Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(AssertionContext context, int min, int max, int[] excluded)
+ : base(context)
+ {
+ _min = min;
+ _max = max;
+ _excluded = excluded;
+ }
+
+ 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!.IsBetweenExcluding(_min, _max, _excluded);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsBetweenExcluding({_min}, {_max}, {_excluded})";
+ }
+}
+
+///
+/// Generated assertion for MeetsLength
+///
+public sealed class String_MeetsLength_Int_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _minLength;
+ private readonly string[] _suffixes;
+
+ public String_MeetsLength_Int_StringArray_Assertion(AssertionContext context, int minLength, string[] suffixes)
+ : base(context)
+ {
+ _minLength = minLength;
+ _suffixes = suffixes;
+ }
+
+ 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!.MeetsLength(_minLength, _suffixes);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy MeetsLength({_minLength}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for IsOneOfWithDefault
+///
+[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+public sealed class T_IsOneOfWithDefault_T_TArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly T _fallback;
+ private readonly T[] _alternatives;
+
+ public T_IsOneOfWithDefault_T_TArray_Assertion(AssertionContext context, T fallback, T[] alternatives)
+ : base(context)
+ {
+ _fallback = fallback;
+ _alternatives = alternatives;
+ }
+
+ 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!.IsOneOfWithDefault(_fallback, _alternatives);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsOneOfWithDefault({_fallback}, {_alternatives})";
+ }
+}
+
+///
+/// Generated assertion for StartsWithAny
+///
+public sealed class String_StartsWithAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _prefix;
+ private readonly string[] _suffixes;
+
+ public String_StartsWithAny_String_StringArray_Assertion(AssertionContext context, string prefix, string[] suffixes)
+ : base(context)
+ {
+ _prefix = prefix;
+ _suffixes = suffixes;
+ }
+
+ 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 = _suffixes.Length >= 0 && value!.StartsWith(_prefix);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy StartsWithAny({_prefix}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for ContainsExactly
+///
+public sealed class String_ContainsExactly_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string[] _required;
+
+ public String_ContainsExactly_StringArray_Assertion(AssertionContext context, string[] required)
+ : base(context)
+ {
+ _required = required;
+ }
+
+ 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!.ContainsExactly(_required);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsExactly({_required})";
+ }
+}
+
+public static partial class ParamsParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for ContainsAny
+ ///
+ public static String_ContainsAny_String_StringArray_Assertion ContainsAny(this IAssertionSource source, string label, [CallerArgumentExpression(nameof(label))] string? labelExpression = null, params string[] candidates)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsAny({labelExpression}, {candidates})");
+ return new String_ContainsAny_String_StringArray_Assertion(source.Context, label, candidates);
+ }
+
+ ///
+ /// Generated extension method for IsBetweenExcluding
+ ///
+ public static Int_IsBetweenExcluding_Int_Int_IntArray_Assertion IsBetweenExcluding(this IAssertionSource source, int min, int max, [CallerArgumentExpression(nameof(min))] string? minExpression = null, [CallerArgumentExpression(nameof(max))] string? maxExpression = null, params int[] excluded)
+ {
+ source.Context.ExpressionBuilder.Append($".IsBetweenExcluding({minExpression}, {maxExpression}, {excluded})");
+ return new Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(source.Context, min, max, excluded);
+ }
+
+ ///
+ /// Generated extension method for MeetsLength
+ ///
+ public static String_MeetsLength_Int_StringArray_Assertion MeetsLength(this IAssertionSource source, int minLength = 1, [CallerArgumentExpression(nameof(minLength))] string? minLengthExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".MeetsLength({minLengthExpression}, {suffixes})");
+ return new String_MeetsLength_Int_StringArray_Assertion(source.Context, minLength, suffixes);
+ }
+
+ ///
+ /// Generated extension method for IsOneOfWithDefault
+ ///
+ [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+ public static T_IsOneOfWithDefault_T_TArray_Assertion IsOneOfWithDefault(this IAssertionSource source, T fallback, [CallerArgumentExpression(nameof(fallback))] string? fallbackExpression = null, params T[] alternatives)
+ {
+ source.Context.ExpressionBuilder.Append($".IsOneOfWithDefault({fallbackExpression}, {alternatives})");
+ return new T_IsOneOfWithDefault_T_TArray_Assertion(source.Context, fallback, alternatives);
+ }
+
+ ///
+ /// Generated extension method for StartsWithAny
+ ///
+ public static String_StartsWithAny_String_StringArray_Assertion StartsWithAny(this IAssertionSource source, string prefix, [CallerArgumentExpression(nameof(prefix))] string? prefixExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".StartsWithAny({prefixExpression}, {suffixes})");
+ return new String_StartsWithAny_String_StringArray_Assertion(source.Context, prefix, suffixes);
+ }
+
+ ///
+ /// Generated extension method for ContainsExactly
+ ///
+ public static String_ContainsExactly_StringArray_Assertion ContainsExactly(this IAssertionSource source, params string[] required)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsExactly({required})");
+ return new String_ContainsExactly_StringArray_Assertion(source.Context, required);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet9_0.verified.txt
new file mode 100644
index 0000000000..707c766c63
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.DotNet9_0.verified.txt
@@ -0,0 +1,328 @@
+[
+//
+#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 ContainsAny
+///
+public sealed class String_ContainsAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _label;
+ private readonly string[] _candidates;
+
+ public String_ContainsAny_String_StringArray_Assertion(AssertionContext context, string label, string[] candidates)
+ : base(context)
+ {
+ _label = label;
+ _candidates = candidates;
+ }
+
+ 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!.ContainsAny(_label, _candidates);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsAny({_label}, {_candidates})";
+ }
+}
+
+///
+/// Generated assertion for IsBetweenExcluding
+///
+public sealed class Int_IsBetweenExcluding_Int_Int_IntArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _min;
+ private readonly int _max;
+ private readonly int[] _excluded;
+
+ public Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(AssertionContext context, int min, int max, int[] excluded)
+ : base(context)
+ {
+ _min = min;
+ _max = max;
+ _excluded = excluded;
+ }
+
+ 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!.IsBetweenExcluding(_min, _max, _excluded);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsBetweenExcluding({_min}, {_max}, {_excluded})";
+ }
+}
+
+///
+/// Generated assertion for MeetsLength
+///
+public sealed class String_MeetsLength_Int_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _minLength;
+ private readonly string[] _suffixes;
+
+ public String_MeetsLength_Int_StringArray_Assertion(AssertionContext context, int minLength, string[] suffixes)
+ : base(context)
+ {
+ _minLength = minLength;
+ _suffixes = suffixes;
+ }
+
+ 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!.MeetsLength(_minLength, _suffixes);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy MeetsLength({_minLength}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for IsOneOfWithDefault
+///
+[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+public sealed class T_IsOneOfWithDefault_T_TArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly T _fallback;
+ private readonly T[] _alternatives;
+
+ public T_IsOneOfWithDefault_T_TArray_Assertion(AssertionContext context, T fallback, T[] alternatives)
+ : base(context)
+ {
+ _fallback = fallback;
+ _alternatives = alternatives;
+ }
+
+ 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!.IsOneOfWithDefault(_fallback, _alternatives);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsOneOfWithDefault({_fallback}, {_alternatives})";
+ }
+}
+
+///
+/// Generated assertion for StartsWithAny
+///
+public sealed class String_StartsWithAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _prefix;
+ private readonly string[] _suffixes;
+
+ public String_StartsWithAny_String_StringArray_Assertion(AssertionContext context, string prefix, string[] suffixes)
+ : base(context)
+ {
+ _prefix = prefix;
+ _suffixes = suffixes;
+ }
+
+ 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 = _suffixes.Length >= 0 && value!.StartsWith(_prefix);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy StartsWithAny({_prefix}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for ContainsExactly
+///
+public sealed class String_ContainsExactly_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string[] _required;
+
+ public String_ContainsExactly_StringArray_Assertion(AssertionContext context, string[] required)
+ : base(context)
+ {
+ _required = required;
+ }
+
+ 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!.ContainsExactly(_required);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsExactly({_required})";
+ }
+}
+
+public static partial class ParamsParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for ContainsAny
+ ///
+ public static String_ContainsAny_String_StringArray_Assertion ContainsAny(this IAssertionSource source, string label, [CallerArgumentExpression(nameof(label))] string? labelExpression = null, params string[] candidates)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsAny({labelExpression}, {candidates})");
+ return new String_ContainsAny_String_StringArray_Assertion(source.Context, label, candidates);
+ }
+
+ ///
+ /// Generated extension method for IsBetweenExcluding
+ ///
+ public static Int_IsBetweenExcluding_Int_Int_IntArray_Assertion IsBetweenExcluding(this IAssertionSource source, int min, int max, [CallerArgumentExpression(nameof(min))] string? minExpression = null, [CallerArgumentExpression(nameof(max))] string? maxExpression = null, params int[] excluded)
+ {
+ source.Context.ExpressionBuilder.Append($".IsBetweenExcluding({minExpression}, {maxExpression}, {excluded})");
+ return new Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(source.Context, min, max, excluded);
+ }
+
+ ///
+ /// Generated extension method for MeetsLength
+ ///
+ public static String_MeetsLength_Int_StringArray_Assertion MeetsLength(this IAssertionSource source, int minLength = 1, [CallerArgumentExpression(nameof(minLength))] string? minLengthExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".MeetsLength({minLengthExpression}, {suffixes})");
+ return new String_MeetsLength_Int_StringArray_Assertion(source.Context, minLength, suffixes);
+ }
+
+ ///
+ /// Generated extension method for IsOneOfWithDefault
+ ///
+ [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+ public static T_IsOneOfWithDefault_T_TArray_Assertion IsOneOfWithDefault(this IAssertionSource source, T fallback, [CallerArgumentExpression(nameof(fallback))] string? fallbackExpression = null, params T[] alternatives)
+ {
+ source.Context.ExpressionBuilder.Append($".IsOneOfWithDefault({fallbackExpression}, {alternatives})");
+ return new T_IsOneOfWithDefault_T_TArray_Assertion(source.Context, fallback, alternatives);
+ }
+
+ ///
+ /// Generated extension method for StartsWithAny
+ ///
+ public static String_StartsWithAny_String_StringArray_Assertion StartsWithAny(this IAssertionSource source, string prefix, [CallerArgumentExpression(nameof(prefix))] string? prefixExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".StartsWithAny({prefixExpression}, {suffixes})");
+ return new String_StartsWithAny_String_StringArray_Assertion(source.Context, prefix, suffixes);
+ }
+
+ ///
+ /// Generated extension method for ContainsExactly
+ ///
+ public static String_ContainsExactly_StringArray_Assertion ContainsExactly(this IAssertionSource source, params string[] required)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsExactly({required})");
+ return new String_ContainsExactly_StringArray_Assertion(source.Context, required);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.Net4_7.verified.txt
new file mode 100644
index 0000000000..707c766c63
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.ParamsParameter.Net4_7.verified.txt
@@ -0,0 +1,328 @@
+[
+//
+#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 ContainsAny
+///
+public sealed class String_ContainsAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _label;
+ private readonly string[] _candidates;
+
+ public String_ContainsAny_String_StringArray_Assertion(AssertionContext context, string label, string[] candidates)
+ : base(context)
+ {
+ _label = label;
+ _candidates = candidates;
+ }
+
+ 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!.ContainsAny(_label, _candidates);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsAny({_label}, {_candidates})";
+ }
+}
+
+///
+/// Generated assertion for IsBetweenExcluding
+///
+public sealed class Int_IsBetweenExcluding_Int_Int_IntArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _min;
+ private readonly int _max;
+ private readonly int[] _excluded;
+
+ public Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(AssertionContext context, int min, int max, int[] excluded)
+ : base(context)
+ {
+ _min = min;
+ _max = max;
+ _excluded = excluded;
+ }
+
+ 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!.IsBetweenExcluding(_min, _max, _excluded);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsBetweenExcluding({_min}, {_max}, {_excluded})";
+ }
+}
+
+///
+/// Generated assertion for MeetsLength
+///
+public sealed class String_MeetsLength_Int_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly int _minLength;
+ private readonly string[] _suffixes;
+
+ public String_MeetsLength_Int_StringArray_Assertion(AssertionContext context, int minLength, string[] suffixes)
+ : base(context)
+ {
+ _minLength = minLength;
+ _suffixes = suffixes;
+ }
+
+ 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!.MeetsLength(_minLength, _suffixes);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy MeetsLength({_minLength}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for IsOneOfWithDefault
+///
+[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+public sealed class T_IsOneOfWithDefault_T_TArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly T _fallback;
+ private readonly T[] _alternatives;
+
+ public T_IsOneOfWithDefault_T_TArray_Assertion(AssertionContext context, T fallback, T[] alternatives)
+ : base(context)
+ {
+ _fallback = fallback;
+ _alternatives = alternatives;
+ }
+
+ 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!.IsOneOfWithDefault(_fallback, _alternatives);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy IsOneOfWithDefault({_fallback}, {_alternatives})";
+ }
+}
+
+///
+/// Generated assertion for StartsWithAny
+///
+public sealed class String_StartsWithAny_String_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string _prefix;
+ private readonly string[] _suffixes;
+
+ public String_StartsWithAny_String_StringArray_Assertion(AssertionContext context, string prefix, string[] suffixes)
+ : base(context)
+ {
+ _prefix = prefix;
+ _suffixes = suffixes;
+ }
+
+ 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 = _suffixes.Length >= 0 && value!.StartsWith(_prefix);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy StartsWithAny({_prefix}, {_suffixes})";
+ }
+}
+
+///
+/// Generated assertion for ContainsExactly
+///
+public sealed class String_ContainsExactly_StringArray_Assertion : Assertion
+{
+ private static readonly Task _passedTask = Task.FromResult(AssertionResult.Passed);
+
+ private readonly string[] _required;
+
+ public String_ContainsExactly_StringArray_Assertion(AssertionContext context, string[] required)
+ : base(context)
+ {
+ _required = required;
+ }
+
+ 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!.ContainsExactly(_required);
+ return result
+ ? _passedTask
+ : Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation()
+ {
+ return $"to satisfy ContainsExactly({_required})";
+ }
+}
+
+public static partial class ParamsParameterAssertionExtensions
+{
+ ///
+ /// Generated extension method for ContainsAny
+ ///
+ public static String_ContainsAny_String_StringArray_Assertion ContainsAny(this IAssertionSource source, string label, [CallerArgumentExpression(nameof(label))] string? labelExpression = null, params string[] candidates)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsAny({labelExpression}, {candidates})");
+ return new String_ContainsAny_String_StringArray_Assertion(source.Context, label, candidates);
+ }
+
+ ///
+ /// Generated extension method for IsBetweenExcluding
+ ///
+ public static Int_IsBetweenExcluding_Int_Int_IntArray_Assertion IsBetweenExcluding(this IAssertionSource source, int min, int max, [CallerArgumentExpression(nameof(min))] string? minExpression = null, [CallerArgumentExpression(nameof(max))] string? maxExpression = null, params int[] excluded)
+ {
+ source.Context.ExpressionBuilder.Append($".IsBetweenExcluding({minExpression}, {maxExpression}, {excluded})");
+ return new Int_IsBetweenExcluding_Int_Int_IntArray_Assertion(source.Context, min, max, excluded);
+ }
+
+ ///
+ /// Generated extension method for MeetsLength
+ ///
+ public static String_MeetsLength_Int_StringArray_Assertion MeetsLength(this IAssertionSource source, int minLength = 1, [CallerArgumentExpression(nameof(minLength))] string? minLengthExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".MeetsLength({minLengthExpression}, {suffixes})");
+ return new String_MeetsLength_Int_StringArray_Assertion(source.Context, minLength, suffixes);
+ }
+
+ ///
+ /// Generated extension method for IsOneOfWithDefault
+ ///
+ [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")]
+ public static T_IsOneOfWithDefault_T_TArray_Assertion IsOneOfWithDefault(this IAssertionSource source, T fallback, [CallerArgumentExpression(nameof(fallback))] string? fallbackExpression = null, params T[] alternatives)
+ {
+ source.Context.ExpressionBuilder.Append($".IsOneOfWithDefault({fallbackExpression}, {alternatives})");
+ return new T_IsOneOfWithDefault_T_TArray_Assertion(source.Context, fallback, alternatives);
+ }
+
+ ///
+ /// Generated extension method for StartsWithAny
+ ///
+ public static String_StartsWithAny_String_StringArray_Assertion StartsWithAny(this IAssertionSource source, string prefix, [CallerArgumentExpression(nameof(prefix))] string? prefixExpression = null, params string[] suffixes)
+ {
+ source.Context.ExpressionBuilder.Append($".StartsWithAny({prefixExpression}, {suffixes})");
+ return new String_StartsWithAny_String_StringArray_Assertion(source.Context, prefix, suffixes);
+ }
+
+ ///
+ /// Generated extension method for ContainsExactly
+ ///
+ public static String_ContainsExactly_StringArray_Assertion ContainsExactly(this IAssertionSource source, params string[] required)
+ {
+ source.Context.ExpressionBuilder.Append($".ContainsExactly({required})");
+ return new String_ContainsExactly_StringArray_Assertion(source.Context, required);
+ }
+
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
index 06bd514fd0..47bd63421d 100644
--- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
+++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.cs
@@ -339,4 +339,60 @@ public Task ArrayTargetType() => RunTest(
await Assert.That(mainFile!).Contains("IAssertionSource");
await Assert.That(mainFile!).Contains("StringArray_ContainsMessage_String_Bool_Assertion");
});
+
+ [Test]
+ public Task ParamsParameter() => RunTest(
+ Path.Combine(Sourcy.Git.RootDirectory.FullName,
+ "TUnit.Assertions.SourceGenerator.Tests",
+ "TestData",
+ "ParamsParameterAssertion.cs"),
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles).Count().IsEqualTo(1);
+
+ var mainFile = generatedFiles.First();
+ await Assert.That(mainFile).IsNotNull();
+
+ // Edge 1: canonical CS0231 shape: required parameter followed by params.
+ // The [CallerArgumentExpression(nameof(label))] block must sit BEFORE the
+ // params array; if the order ever regresses, this single substring fails
+ // ahead of the compile-clean gate below.
+ await Assert.That(mainFile).Contains(
+ "ContainsAny(this IAssertionSource source, string label, [CallerArgumentExpression(nameof(label))] string? labelExpression = null, params string[] candidates)");
+
+ // Edge 2: multiple required parameters preceding params. Every diagnostic
+ // parameter is emitted, in source order, ahead of the params array.
+ await Assert.That(mainFile).Contains(
+ "IsBetweenExcluding(this IAssertionSource source, int min, int max, [CallerArgumentExpression(nameof(min))] string? minExpression = null, [CallerArgumentExpression(nameof(max))] string? maxExpression = null, params int[] excluded)");
+
+ // Edge 3: optional defaulted parameter before params. The default value
+ // is preserved and the diagnostic parameter still precedes params.
+ await Assert.That(mainFile).Contains(
+ "MeetsLength(this IAssertionSource source, int minLength = 1, [CallerArgumentExpression(nameof(minLength))] string? minLengthExpression = null, params string[] suffixes)");
+
+ // Edge 4: generic-typed parameters. The generic substitutions are
+ // preserved and the diagnostic for the required generic param precedes
+ // the generic params array.
+ await Assert.That(mainFile).Contains(
+ "IsOneOfWithDefault(this IAssertionSource source, T fallback, [CallerArgumentExpression(nameof(fallback))] string? fallbackExpression = null, params T[] alternatives)");
+
+ // Edge 5: InlineMethodBody path. The same emit ordering applies when
+ // the body is inlined (not delegated through a stored helper).
+ await Assert.That(mainFile).Contains(
+ "StartsWithAny(this IAssertionSource source, string prefix, [CallerArgumentExpression(nameof(prefix))] string? prefixExpression = null, params string[] suffixes)");
+
+ // Edge 6: params-only. Byte-identical to the pre-fix shape used by
+ // IsIn/IsNotIn in production. No diagnostic parameter is emitted at
+ // all because the params parameter itself cannot be auto-supplied,
+ // and there is no preceding non-params parameter that could.
+ await Assert.That(mainFile).Contains(
+ "ContainsExactly(this IAssertionSource source, params string[] required)");
+ await Assert.That(mainFile).DoesNotContain("requiredExpression");
+
+ // Structural gate: parse the emitted code as C# and fail on any error
+ // diagnostic. This is the regression sentinel for CS0231 plus any other
+ // emit defect (mis-paired brackets, invalid generic argument lists, etc.)
+ // that a content-only assertion cannot see.
+ await CompileChecker.AssertNoErrors(generatedFiles);
+ });
}
diff --git a/TUnit.Assertions.SourceGenerator.Tests/TestData/ParamsParameterAssertion.cs b/TUnit.Assertions.SourceGenerator.Tests/TestData/ParamsParameterAssertion.cs
new file mode 100644
index 0000000000..3b5e12f993
--- /dev/null
+++ b/TUnit.Assertions.SourceGenerator.Tests/TestData/ParamsParameterAssertion.cs
@@ -0,0 +1,46 @@
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Tests.TestData;
+
+///
+/// Test case: [GenerateAssertion] methods whose source signature contains a
+/// params array. The generator must emit any auto-supplied
+/// [CallerArgumentExpression] diagnostic parameter BEFORE the
+/// params parameter; otherwise the generated extension method violates
+/// CS0231 ("A params parameter must be the last parameter in a parameter list").
+///
+public static partial class ParamsParameterAssertionExtensions
+{
+ // Canonical CS0231 bug shape: one required source parameter followed by params.
+ [GenerateAssertion]
+ public static bool ContainsAny(this string value, string label, params string[] candidates)
+ => candidates.Length > 0 && value.Length >= label.Length;
+
+ // Multiple required source parameters preceding params: each must contribute
+ // its own [CallerArgumentExpression] in the diagnostic block before params.
+ [GenerateAssertion]
+ public static bool IsBetweenExcluding(this int value, int min, int max, params int[] excluded)
+ => value >= min && value <= max && excluded.Length >= 0;
+
+ // Optional defaulted source parameter before params. C# allows
+ // (T x = default, params T[] y); the generator must preserve the default.
+ [GenerateAssertion]
+ public static bool MeetsLength(this string value, int minLength = 1, params string[] suffixes)
+ => value.Length >= minLength && suffixes.Length >= 0;
+
+ // Generic-typed required parameter alongside a generic-typed params array.
+ [GenerateAssertion]
+ public static bool IsOneOfWithDefault(this T value, T fallback, params T[] alternatives)
+ => alternatives.Length >= 0 || EqualityComparer.Default.Equals(fallback, value);
+
+ // InlineMethodBody must follow the same emit order on the inline path.
+ [GenerateAssertion(InlineMethodBody = true)]
+ public static bool StartsWithAny(this string value, string prefix, params string[] suffixes)
+ => suffixes.Length >= 0 && value.StartsWith(prefix);
+
+ // Regression: params-only (no preceding source parameter) must remain
+ // byte-identical to the pre-fix shape used by IsIn/IsNotIn in production.
+ [GenerateAssertion]
+ public static bool ContainsExactly(this string value, params string[] required)
+ => required.Length >= 0 && value.Length >= 0;
+}
diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
index fe674ec808..114d1010a8 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs
@@ -1042,25 +1042,39 @@ private static void GenerateExtensionMethod(StringBuilder sb, AssertionMethodDat
var sourceTypeName = isCovariant ? covariantParam! : targetTypeName;
sb.Append($"this IAssertionSource<{sourceTypeName}> source");
- // Additional parameters
+ // Non-params source parameters first, then their CallerArgumentExpression diagnostic
+ // parameters, then any params parameter last. The compiler requires params to be the
+ // final entry in the list (CS0231), so the CallerArgumentExpression block must precede
+ // it instead of trailing the whole signature.
foreach (var param in data.AdditionalParameters)
{
- var paramsModifier = param.IsParams ? "params " : "";
- sb.Append($", {paramsModifier}{param.Type} {param.Name}");
+ if (param.IsParams)
+ {
+ continue;
+ }
+ sb.Append($", {param.Type} {param.Name}");
if (param.HasExplicitDefaultValue)
{
sb.Append($" = {param.DefaultValueExpression}");
}
}
- // CallerArgumentExpression parameters (skip for params since params must be last)
- for (int i = 0; i < data.AdditionalParameters.Count; i++)
+ foreach (var param in data.AdditionalParameters)
+ {
+ if (param.IsParams)
+ {
+ continue;
+ }
+ sb.Append($", [CallerArgumentExpression(nameof({param.Name}))] string? {param.Name}Expression = null");
+ }
+
+ foreach (var param in data.AdditionalParameters)
{
- var param = data.AdditionalParameters[i];
if (!param.IsParams)
{
- sb.Append($", [CallerArgumentExpression(nameof({param.Name}))] string? {param.Name}Expression = null");
+ continue;
}
+ sb.Append($", params {param.Type} {param.Name}");
}
sb.AppendLine(")");