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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions Dummies.UnitTests/AnyPatternTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
#region Usings declarations

using System.Text.RegularExpressions;

using NFluent;

#endregion

namespace Dummies.UnitTests;

public sealed class AnyPatternTests {

#region Statics members declarations

private const int SampleCount = 200;

// The oracle: a generated value is correct iff the REAL .NET regex engine fully matches it. Anchoring with
// ^(?:...)$ turns the partial-match IsMatch into a whole-string test, so it catches both under-generation
// (too few characters) and over-generation (trailing junk), and handles top-level alternation correctly.
private static void AssertMatches(string value, string pattern, RegexOptions options = RegexOptions.None) {
Assert.True(Regex.IsMatch(value, "^(?:" + pattern + ")$", options),
$"generated value {Display(value)} is not matched by /{pattern}/");
}

private static string Display(string value) {
return "\"" + value.Replace("\t", "\\t").Replace("\n", "\\n").Replace("\r", "\\r") + "\"";
}

#endregion

[Theory(DisplayName = "Every generated value is fully matched by the real .NET regex engine.")]
[InlineData(@"\d{8}")]
[InlineData(@"^ORD-\d{8}$")]
[InlineData(@"[A-Z]{3}")]
[InlineData(@"[a-z]{2,5}")]
[InlineData(@"(EUR|USD|GBP)")]
[InlineData(@"[A-Za-z0-9_]+")]
[InlineData(@"\w{4}\d{2}")]
[InlineData(@"[^0-9]{3}")]
[InlineData(@"colou?r")]
[InlineData(@"a{2,4}b*c+")]
[InlineData(@"(ab|cd){2,3}")]
[InlineData(@"\d+\.\d{2}")]
[InlineData(@"[A-F0-9]{6}")]
[InlineData(@"(?:foo|bar)-\d+")]
[InlineData(@"(?<year>\d{4})-(?<month>\d{2})")]
[InlineData(@"(?'tag'\d{2})")]
[InlineData(@"^a$|^b$")]
[InlineData(@"[\d]{3}")]
[InlineData(@"[-a-z]{2}")]
[InlineData(@"[a-z-]{2}")]
[InlineData(@"[a-b-z]{4}")]
[InlineData(@"[]a]{3}")]
[InlineData(@"[^]]{3}")]
[InlineData(@"[\b]")]
[InlineData(@"[\1]")]
[InlineData(@"[\x30-\x39]{3}")]
[InlineData(@"(a|aa|aaa)")]
[InlineData(@"a+?b*?")]
[InlineData(@"\x41\x2DB")]
[InlineData(@"\a\t")]
[InlineData(@"\e")]
[InlineData(@"\cM")]
[InlineData(@"\0")]
[InlineData(@"\07")]
[InlineData(@"a{x}")]
[InlineData(@"{abc}")]
[InlineData(@"a{2,")]
[InlineData(@"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")]
[InlineData(@"[A-Z]{2}\d{2}[A-Z0-9]{10}")]
[InlineData(@"([01]\d|2[0-3]):[0-5]\d")]
[InlineData(@"\d+\.\d+\.\d+(-[a-z]+(\.\d+)?)?")]
[InlineData(@"\s")]
[InlineData(@".")]
[InlineData(@"")]
public void GeneratedValuesMatchTheRealEngine(string pattern) {
AnyContext context = Any.WithSeed(20260718);
AnyPattern generator = context.StringMatching(pattern);

for (int i = 0; i < SampleCount; i++) {
AssertMatches(generator.Generate(), pattern);
}
}

[Fact(DisplayName = "Generated values vary from draw to draw whenever the pattern leaves room.")]
public void GeneratedValuesVary() {
foreach (string pattern in new[] { @"\d{8}", @"[A-Z]{3}", @"(EUR|USD|GBP)", @"[A-Za-z0-9_]+", @"a{2,4}b*c+" }) {
AnyPattern generator = Any.WithSeed(4242).StringMatching(pattern);
HashSet<string> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); }
Check.That(seen.Count).IsStrictlyGreaterThan(1);
}
}

[Fact(DisplayName = "A fixed-shape pattern yields exactly that shape.")]
public void FixedShape() {
for (int i = 0; i < SampleCount; i++) {
string reference = Any.StringMatching(@"^ORD-\d{8}$").Generate();
Check.That(reference.Length).IsEqualTo(12);
Check.That(reference).StartsWith("ORD-");
Check.That(reference.Substring(4)).Matches("^[0-9]{8}$");
}
}

[Fact(DisplayName = "Alternation draws each branch and only declared branches.")]
public void Alternation() {
HashSet<string> seen = new();
for (int i = 0; i < SampleCount; i++) {
string value = Any.StringMatching("(EUR|USD|GBP)").Generate();
Check.That(value == "EUR" || value == "USD" || value == "GBP").IsTrue();
seen.Add(value);
}

Check.That(seen).Contains("EUR", "USD", "GBP");
}

[Fact(DisplayName = "Character classes, ranges and negation stay within their set.")]
public void CharacterClasses() {
for (int i = 0; i < SampleCount; i++) {
Check.That(Any.StringMatching("[A-Z]{3}").Generate()).Matches("^[A-Z]{3}$");
Check.That(Any.StringMatching("[^0-9]{4}").Generate()).Matches("^[^0-9]{4}$");
Check.That(Any.StringMatching(@"[\d]{5}").Generate()).Matches("^[0-9]{5}$");
}
}

[Fact(DisplayName = "Bounded quantifiers stay within their bounds; unbounded ones draw the minimum plus 0 to 8.")]
public void QuantifierBounds() {
HashSet<int> starLengths = new();
HashSet<int> plusLengths = new();
HashSet<int> openLengths = new();

for (int i = 0; i < SampleCount; i++) {
int bounded = (Any.StringMatching("a{2,4}").Generate()).Length;
Check.That(bounded is >= 2 and <= 4).IsTrue();

starLengths.Add((Any.StringMatching("a*").Generate()).Length);
plusLengths.Add((Any.StringMatching("a+").Generate()).Length);
openLengths.Add((Any.StringMatching("a{2,}").Generate()).Length);
}

Check.That(starLengths.Min()).IsEqualTo(0);
Check.That(starLengths.Max()).IsEqualTo(8); // 0 + 0..8
Check.That(plusLengths.Min()).IsEqualTo(1);
Check.That(plusLengths.Max()).IsEqualTo(9); // 1 + 0..8
Check.That(openLengths.Min()).IsEqualTo(2);
Check.That(openLengths.Max()).IsEqualTo(10); // 2 + 0..8
}

[Fact(DisplayName = "Anchors are no-ops: the whole generated string is the match.")]
public void AnchorsAreNoOps() {
for (int i = 0; i < SampleCount; i++) {
Check.That(Any.StringMatching("^abc$").Generate()).IsEqualTo("abc");
}
}

[Fact(DisplayName = "A Regex with IgnoreCase generates either case.")]
public void IgnoreCaseHonoured() {
Regex pattern = new("^[a-z]{5}$", RegexOptions.IgnoreCase);
bool sawUpper = false;
AnyContext context = Any.WithSeed(99);
AnyPattern generator = context.StringMatching(pattern);

for (int i = 0; i < SampleCount; i++) {
string value = generator.Generate();
AssertMatches(value, "[a-z]{5}", RegexOptions.IgnoreCase);
if (value.Any(char.IsUpper)) { sawUpper = true; }
}

Check.That(sawUpper).IsTrue();
}

[Fact(DisplayName = "A matching generator composes into a value object through As.")]
public void ComposesThroughAs() {
IAny<OrderReference> generator = Any.StringMatching(@"^ORD-\d{8}$").As(OrderReference.Create);

for (int i = 0; i < 50; i++) {
OrderReference reference = generator.Generate();
Check.That(reference.Value).StartsWith("ORD-");
Check.That(reference.Value.Length).IsEqualTo(12);
}
}

[Fact(DisplayName = "Matching is reproducible under a seed.")]
public void ReproducibleUnderASeed() {
string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).StringMatching(@"[A-Z]{3}-\d{4}").Generate()));
string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).StringMatching(@"[A-Z]{3}-\d{4}").Generate()));

Check.That(second).IsEqualTo(first);
}

[Fact(DisplayName = "Non-regular constructs are refused eagerly with UnsupportedRegexException.")]
public void UnsupportedConstructsAreRefused() {
Check.ThatCode(() => Any.StringMatching(@"foo(?=bar)")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"foo(?!bar)")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"(?<=x)y")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"\bword\b")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"(\w+)\s\1")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"\p{L}+")).Throws<UnsupportedRegexException>();

UnsupportedRegexException caught = Assert.Throws<UnsupportedRegexException>(() => Any.StringMatching(@"a(?=b)"));
Check.That(caught.Message).Contains("lookahead");
}

[Fact(DisplayName = "Constructs whose language a plain walk cannot honour are refused, never mis-generated.")]
public void NotGeneratableConstructsAreRefused() {
// An atomic group commits to its first matching branch: (?>ab|a)b matches only "abb", so lowering it to
// a plain alternation could emit "ab" — refused instead.
Check.ThatCode(() => Any.StringMatching(@"(?>ab|a)b")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"(?>a)")).Throws<UnsupportedRegexException>();

// A misplaced anchor makes the pattern unmatchable by any whole string.
Check.ThatCode(() => Any.StringMatching(@"a^")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"$a")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"x(^a)")).Throws<UnsupportedRegexException>();
Check.ThatCode(() => Any.StringMatching(@"(a$)x")).Throws<UnsupportedRegexException>();

// .NET class subtraction removes a nested class; parsing '-[' as members would generate outside the set.
Check.ThatCode(() => Any.StringMatching(@"[a-z-[aeiou]]")).Throws<UnsupportedRegexException>();

// IgnorePatternWhitespace changes how the pattern text itself is read: "^A B$" matches "AB", not "A B".
Check.ThatCode(() => Any.StringMatching(new Regex("^A B$", RegexOptions.IgnorePatternWhitespace))).Throws<ArgumentException>();
Check.ThatCode(() => Any.WithSeed(1).StringMatching(new Regex("^A B$", RegexOptions.IgnorePatternWhitespace))).Throws<ArgumentException>();
}

[Fact(DisplayName = "Malformed patterns raise ArgumentException; a null pattern raises ArgumentNullException.")]
public void MalformedPatternsAreRejected() {
Check.ThatCode(() => Any.StringMatching(@"[a-")).Throws<ArgumentException>();
Check.ThatCode(() => Any.StringMatching(@"(abc")).Throws<ArgumentException>();
Check.ThatCode(() => Any.StringMatching(@"a{3,1}")).Throws<ArgumentException>();
Check.ThatCode(() => Any.StringMatching(@"*abc")).Throws<ArgumentException>();
Check.ThatCode(() => Any.StringMatching(@"a\")).Throws<ArgumentException>();
Check.ThatCode(() => Any.StringMatching((string)null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => Any.StringMatching((Regex)null!)).Throws<ArgumentNullException>();
}

[Fact(DisplayName = "Patterns the real engine rejects are rejected here too, never silently re-interpreted.")]
public void RealEngineRejectionsAreMirrored() {
// Each of these is refused by System.Text.RegularExpressions; accepting them would make the generator
// produce values for patterns no production code could ever carry.
Check.ThatCode(() => Any.StringMatching(@"a*+")).Throws<ArgumentException>(); // possessive: not a .NET construct
Check.ThatCode(() => Any.StringMatching(@"a**")).Throws<ArgumentException>(); // nested quantifier
Check.ThatCode(() => Any.StringMatching(@"a*??")).Throws<ArgumentException>(); // nested quantifier
Check.ThatCode(() => Any.StringMatching(@"[]")).Throws<ArgumentException>(); // unterminated class
Check.ThatCode(() => Any.StringMatching(@"\q")).Throws<ArgumentException>(); // unrecognized escape
Check.ThatCode(() => Any.StringMatching(@"\x4")).Throws<ArgumentException>(); // \x expects 2 hex digits
Check.ThatCode(() => Any.StringMatching(@"\c1")).Throws<ArgumentException>(); // \c expects a letter
Check.ThatCode(() => Any.StringMatching(@"{2}")).Throws<ArgumentException>(); // quantifier following nothing
Check.ThatCode(() => Any.StringMatching(@"(?<>a)")).Throws<ArgumentException>(); // empty group name
}

[Fact(DisplayName = "Escape sequences generate the real characters, not their letter.")]
public void EscapesGenerateTheRealCharacters() {
Check.That(Any.StringMatching(@"\a").Generate()).IsEqualTo("\a");
Check.That(Any.StringMatching(@"\e").Generate()).IsEqualTo("\u001B");
Check.That(Any.StringMatching(@"\x41").Generate()).IsEqualTo("A");
Check.That(Any.StringMatching(@"\u0042").Generate()).IsEqualTo("B");
Check.That(Any.StringMatching(@"\cA").Generate()).IsEqualTo("\u0001");
Check.That(Any.StringMatching(@"\07").Generate()).IsEqualTo("\a");
Check.That(Any.StringMatching(@"\0").Generate()).IsEqualTo("\0");
}

[Fact(DisplayName = "A brace that is not a well-formed quantifier is a literal, exactly as in the real engine.")]
public void BraceLiteralsGenerate() {
Check.That(Any.StringMatching(@"a{x}").Generate()).IsEqualTo("a{x}");
Check.That(Any.StringMatching(@"{abc}").Generate()).IsEqualTo("{abc}");
Check.That(Any.StringMatching(@"a{2,").Generate()).IsEqualTo("a{2,");
}

[Fact(DisplayName = "Nesting groups beyond the parser's depth ceiling fails cleanly instead of overflowing the stack.")]
public void DeepNestingFailsCleanly() {
string deep = new string('(', 300) + "a" + new string(')', 300);

ArgumentException caught = Assert.Throws<ArgumentException>(() => Any.StringMatching(deep));
Check.That(caught.Message).Contains("nested");
}

}
3 changes: 2 additions & 1 deletion Dummies.UnitTests/SeedReproducibilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ private static string Batch() {
List<int> list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4).Generate();
HashSet<int> set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3).Generate();
int? maybe = Any.Int32().Between(0, 9).OrNull().Generate();
string coded = Any.StringMatching(@"[A-Z]{3}-\d{4}").Generate();

return string.Join("|", full, bounded, free, capped, shaped,
wide, unsigned, real, exact, flag, id, letter,
span.Ticks, instant.Ticks, huge, tiny,
string.Join("-", list), string.Join("-", set.OrderBy(value => value)),
maybe?.ToString() ?? "null");
maybe?.ToString() ?? "null", coded);
}

#endregion
Expand Down
54 changes: 54 additions & 0 deletions Dummies/Any.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
#region Usings declarations

using System.Text.RegularExpressions;

#endregion

namespace Dummies;

/// <summary>
Expand Down Expand Up @@ -49,6 +55,54 @@
return new AnyString(AmbientRandomSource.Instance, StringSpec.Unconstrained);
}

/// <summary>
/// Starts a generator of arbitrary strings that <b>match <paramref name="pattern" /></b>, drawing from the
/// ambient random context. The pattern is the whole specification — the returned generator carries no further
/// shape or length constraints; express those inside the pattern. It still composes through <c>As(...)</c>,
/// <c>OrNull()</c>, <c>Combine(...)</c> and the collection generators.
/// </summary>
/// <remarks>
/// Supported is the <b>regular</b> subset of the pattern language: literals and escapes (metacharacters,
/// control characters, <c>\xHH</c>, <c>\uHHHH</c>), the shorthands <c>\d \D \w \W \s \S</c>, character classes
/// (ranges, negation), the quantifiers <c>? * + {n} {n,} {n,m}</c> (an unbounded quantifier draws its minimum
/// plus 0 to 8 repetitions), alternation, grouping (capturing, non-capturing and named), the dot, and the
/// anchors <c>^ $</c> at the start and end of the pattern or of a top-level alternation branch (no-ops there,
/// since a whole matching string is generated). Values are drawn from printable ASCII. A well-formed but
/// non-regular or not-generatable construct — a lookaround, a backreference, a word boundary, a Unicode
/// category, an atomic group, a class subtraction, an anchor placed where it could never match — raises an
/// <see cref="UnsupportedRegexException" />; a malformed pattern raises an <see cref="ArgumentException" />,
/// mirroring what the real engine rejects.
/// </remarks>
/// <param name="pattern">The regular expression the generated strings must match.</param>
/// <returns>A generator of strings matching the pattern.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="pattern" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="pattern" /> is not a well-formed pattern.</exception>
/// <exception cref="UnsupportedRegexException">Thrown when <paramref name="pattern" /> uses a construct outside the supported regular subset.</exception>
public static AnyPattern StringMatching(string pattern) {
if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); }

return AnyPattern.FromPattern(AmbientRandomSource.Instance, pattern, ignoreCase: false);
}

/// <summary>
/// Starts a generator of arbitrary strings matching <paramref name="pattern" /> — the same contract as
/// <see cref="StringMatching(string)" />, taking a compiled <see cref="Regex" /> so a test can reuse the very
/// object its production code validates with. <see cref="RegexOptions.IgnoreCase" /> is honoured.
/// <see cref="RegexOptions.IgnorePatternWhitespace" /> changes how the pattern text itself is read and is
/// rejected; the remaining options do not change which strings the pattern matches and are ignored.
/// </summary>
/// <param name="pattern">The regular expression the generated strings must match.</param>
/// <returns>A generator of strings matching the pattern.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="pattern" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="pattern" /> is not a well-formed pattern, or carries <see cref="RegexOptions.IgnorePatternWhitespace" />.</exception>
/// <exception cref="UnsupportedRegexException">Thrown when <paramref name="pattern" /> uses a construct outside the supported regular subset.</exception>
public static AnyPattern StringMatching(Regex pattern) {
if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); }
if ((pattern.Options & RegexOptions.IgnorePatternWhitespace) != 0) { throw new ArgumentException("RegexOptions.IgnorePatternWhitespace changes how the pattern text is read; pass the pattern without it (or with its whitespace and comments removed).", nameof(pattern)); }

return AnyPattern.FromPattern(AmbientRandomSource.Instance, pattern.ToString(), (pattern.Options & RegexOptions.IgnoreCase) != 0);
Comment thread
Reefact marked this conversation as resolved.
}

/// <summary>
/// Starts an arbitrary <see cref="int" /> generator drawing from the ambient random context. Unconstrained, it
/// draws from the full <see cref="int" /> range; chain constraints to express what the surrounding code
Expand Down Expand Up @@ -413,7 +467,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, Func<T1, T2, T3, TResult> compose) {

Check warning on line 470 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -445,7 +499,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, Func<T1, T2, T3, T4, TResult> compose) {

Check warning on line 502 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -481,7 +535,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, Func<T1, T2, T3, T4, T5, TResult> compose) {

Check warning on line 538 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -521,7 +575,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, Func<T1, T2, T3, T4, T5, T6, TResult> compose) {

Check warning on line 578 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -567,7 +621,7 @@
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters",
Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")]
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, T7, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, IAny<T7> seventh, Func<T1, T2, T3, T4, T5, T6, T7, TResult> compose) {

Check warning on line 624 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -618,7 +672,7 @@
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters",
Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")]
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, IAny<T7> seventh, IAny<T8> eighth, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> compose) {

Check warning on line 675 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down
Loading