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(")");