From 5974e40840beb050fbc76fc65170744c05ed3411 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 16:26:41 +0000 Subject: [PATCH 1/2] feat(justdummies): guard public/internal arguments against null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every public or internal member — constructor or method — now rejects a null non-nullable reference-type argument with an ArgumentNullException naming the parameter. The class, not the assembly, is the trust boundary: a member validates what crosses it (including the internal Create factories and constructors the public API can never route a null through), and trusts what a validating member already accepted, so a null fails fast instead of surfacing later as a NullReferenceException far from its cause. Exception-type constructors are exempt: they run on the error-handling and logging path, where throwing over a null message would mask the original failure. A reflection-driven convention test (NullArgumentGuardConventionTests) enforces this across the whole surface and holds new members to it automatically. Verifying the internal boundary requires internal access, so InternalsVisibleTo is opened to the contract suite, deliberately relaxing the previously black-box test posture. ReproduciblyAsync now validates its body synchronously rather than deferring the guard into the returned task. Decision recorded in ADR-0045 (Proposed). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KR3opZZYgSiomTKWvC11q8 --- .../JustDummies.UnitTests.csproj | 3 + .../NullArgumentGuardConventionTests.cs | 456 ++++++++++++++++++ JustDummies/Any.cs | 10 +- JustDummies/AnyArray.cs | 4 + JustDummies/AnyBoolean.cs | 2 + JustDummies/AnyByte.cs | 2 + JustDummies/AnyChar.cs | 2 + JustDummies/AnyCollection.cs | 2 + JustDummies/AnyDateOnly.cs | 2 + JustDummies/AnyDateTime.cs | 2 + JustDummies/AnyDateTimeOffset.cs | 2 + JustDummies/AnyDecimal.cs | 2 + JustDummies/AnyDerivation.cs | 11 + JustDummies/AnyDictionary.cs | 5 +- JustDummies/AnyDouble.cs | 2 + JustDummies/AnyEnum.cs | 1 + JustDummies/AnyFtpUri.cs | 2 + JustDummies/AnyGuid.cs | 2 + JustDummies/AnyHalf.cs | 2 + JustDummies/AnyInt128.cs | 2 + JustDummies/AnyInt16.cs | 2 + JustDummies/AnyInt32.cs | 2 + JustDummies/AnyInt64.cs | 2 + JustDummies/AnyList.cs | 4 + JustDummies/AnyMailtoUri.cs | 2 + JustDummies/AnyOneOf.cs | 7 +- JustDummies/AnyPattern.cs | 4 + JustDummies/AnyRelativeUri.cs | 2 + JustDummies/AnySByte.cs | 2 + JustDummies/AnySequence.cs | 4 + JustDummies/AnySet.cs | 4 + JustDummies/AnySingle.cs | 2 + JustDummies/AnyString.cs | 2 + JustDummies/AnyStringOneOf.cs | 2 + JustDummies/AnyTimeOnly.cs | 2 + JustDummies/AnyTimeSpan.cs | 2 + JustDummies/AnyUInt128.cs | 2 + JustDummies/AnyUInt16.cs | 2 + JustDummies/AnyUInt32.cs | 2 + JustDummies/AnyUInt64.cs | 2 + JustDummies/AnyUri.cs | 2 + JustDummies/AnyWebSocketUri.cs | 2 + JustDummies/AnyWebUri.cs | 2 + JustDummies/CollectionState.cs | 17 + JustDummies/ContinuousIntervalSpec.cs | 19 + JustDummies/CountConstraints.cs | 8 + JustDummies/CountSpec.cs | 5 + JustDummies/DecimalIntervalSpec.cs | 17 + JustDummies/JustDummies.csproj | 8 + JustDummies/OrdinalIntervalSpec.cs | 15 + JustDummies/RandomSource.cs | 9 + JustDummies/RegexAlphabet.cs | 1 + JustDummies/RegexNode.cs | 9 + JustDummies/RegexParser.cs | 1 + JustDummies/StringSpec.cs | 16 + JustDummies/UriSpec.cs | 9 + JustDummies/WideIntervalSpec.cs | 15 + ...-and-internal-arguments-against-null.fr.md | 135 ++++++ ...lic-and-internal-arguments-against-null.md | 127 +++++ doc/handwritten/for-maintainers/adr/README.md | 1 + 60 files changed, 983 insertions(+), 4 deletions(-) create mode 100644 JustDummies.UnitTests/NullArgumentGuardConventionTests.cs create mode 100644 doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.fr.md create mode 100644 doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.md diff --git a/JustDummies.UnitTests/JustDummies.UnitTests.csproj b/JustDummies.UnitTests/JustDummies.UnitTests.csproj index 51ba6fe0..81ca385d 100644 --- a/JustDummies.UnitTests/JustDummies.UnitTests.csproj +++ b/JustDummies.UnitTests/JustDummies.UnitTests.csproj @@ -47,6 +47,9 @@ + + diff --git a/JustDummies.UnitTests/NullArgumentGuardConventionTests.cs b/JustDummies.UnitTests/NullArgumentGuardConventionTests.cs new file mode 100644 index 00000000..1bd255a0 --- /dev/null +++ b/JustDummies.UnitTests/NullArgumentGuardConventionTests.cs @@ -0,0 +1,456 @@ +#region Usings declarations + +using System.Collections; +using System.Linq.Expressions; +using System.Reflection; + +using NFluent; + +#endregion + +namespace JustDummies.UnitTests; + +/// +/// The null-argument guard convention, enforced by reflection over the whole library surface: every +/// public or internal member (constructor or method) that takes a non-nullable reference-type +/// argument must reject null with an naming the offending parameter. +/// A caller — even another class of this assembly — is outside the class it calls, so the boundary validates +/// what crosses it; only what a validating member has already accepted is trusted inside. +/// +/// +/// +/// This is a single self-maintaining guard: it discovers members through reflection, so a new generator, +/// factory, or fluent method is held to the convention automatically, with nothing to add here. Value-type +/// and nullable (?) parameters are excluded by design — the former cannot be null, the latter +/// are deliberately optional. Exception types are excluded too: constructing an exception must never itself +/// throw while an error is being handled or logged. +/// +/// +/// Testing the internal boundary (the Create factories and internal constructors the public API can +/// never route a null through) requires reaching internals, which is why JustDummies opens them to +/// this suite — see ADR-0045. The test uses .NET 6+ reflection nullability metadata, so it runs on the +/// modern leg only and is excluded from the net472 support-floor build. +/// +/// +public sealed class NullArgumentGuardConventionTests { + + private static readonly Assembly LibraryAssembly = typeof(Any).Assembly; + private static readonly NullabilityInfoContext Nullability = new(); + private static readonly List Samples = HarvestSamples(); + + [Fact(DisplayName = "Every public/internal member rejects a null non-nullable reference argument with ArgumentNullException.")] + public void EveryPublicOrInternalMemberGuardsItsNonNullableReferenceArguments() { + List violations = []; + List uncovered = []; + + foreach (Type declared in LibraryAssembly.GetTypes()) { + if (!IsInScope(declared)) { continue; } + + Type type = declared; + if (declared.IsGenericTypeDefinition) { + if (declared.IsAbstract) { continue; } // an abstract base (e.g. AnyCollection`3): covered through its concrete subclasses + if (!TryClose(declared, out type!)) { + uncovered.Add($"type {declared.Name}: could not close generic definition"); + continue; + } + } + + foreach (MemberDescriptor member in MembersOf(type)) { + ProcessMember(member, violations, uncovered); + } + } + + // A parameter the harness could not exercise is a hole in the convention's coverage, not a pass: it is + // reported alongside the missing guards so it is either given a sample here or covered by an explicit test, + // never silently skipped. + List failures = uncovered.Select(entry => $"[uncovered] {entry}") + .Concat(violations.Select(entry => $"[missing-guard] {entry}")) + .OrderBy(line => line, StringComparer.Ordinal) + .ToList(); + + Check.WithCustomMessage( + $"Null-argument guard convention — {violations.Count} member(s) missing a guard, " + + $"{uncovered.Count} parameter(s) the harness could not exercise:{Environment.NewLine}" + + string.Join(Environment.NewLine, failures)) + .That(failures) + .IsEmpty(); + } + + #region Member enumeration + + private readonly struct MemberDescriptor(MethodBase invoke, MethodBase classify, Type receiver) { + + public MethodBase Invoke { get; } = invoke; // closed and callable + public MethodBase Classify { get; } = classify; // open enough to read annotations and generic constraints + public Type Receiver { get; } = receiver; // the (closed) type an instance member is called on + + } + + private static IEnumerable MembersOf(Type type) { + bool instantiable = type is { IsAbstract: false, IsInterface: false }; + + if (instantiable) { + foreach (ConstructorInfo ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { + if (IsAccessible(ctor)) { yield return new MemberDescriptor(ctor, ctor, type); } + } + } + + BindingFlags methodFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; + if (type.IsAbstract) { methodFlags |= BindingFlags.DeclaredOnly; } // instance methods of an abstract base are reached through its concrete subclasses + + foreach (MethodInfo method in type.GetMethods(methodFlags)) { + if (method.DeclaringType == typeof(object)) { continue; } + if (method.IsSpecialName) { continue; } // property/event accessors, operators + if (method.Name.Contains('<')) { continue; } // compiler-generated + if (!IsAccessible(method)) { continue; } + if (method is { IsAbstract: true }) { continue; } // no body to reach directly + + if (method.IsGenericMethodDefinition) { + if (!TryClose(method, out MethodInfo? closed)) { continue; } + yield return new MemberDescriptor(closed!, method, type); + } else { + yield return new MemberDescriptor(method, method, type); + } + } + } + + private static bool IsInScope(Type type) { + if (type.IsInterface || type.IsEnum) { return false; } + if (typeof(Delegate).IsAssignableFrom(type)) { return false; } + if (typeof(Exception).IsAssignableFrom(type)) { return false; } + if (type.Name.StartsWith('<')) { return false; } + if (type.GetCustomAttribute() is not null) { return false; } + + if (type.IsNested) { + return type.IsNestedPublic || type.IsNestedAssembly || type.IsNestedFamORAssem; + } + + return type.IsPublic || type.IsNotPublic; // top-level public or internal + } + + private static bool IsAccessible(MethodBase member) { + return member.IsPublic || member.IsAssembly || member.IsFamilyOrAssembly; // public / internal / protected internal + } + + #endregion + + #region Per-member verification + + private static void ProcessMember(MemberDescriptor member, List violations, List uncovered) { + ParameterInfo[] invokeParams = member.Invoke.GetParameters(); + ParameterInfo[] classifyParams = ClassificationParameters(member); + + // A validation helper that carries a caller-supplied name (RequireHost(host, parameterName)) reports that + // forwarded name, not its own — so for such members any ArgumentNullException, whatever its ParamName, honours + // the convention. Whether an exception is thrown at all is still checked strictly. + bool forwardsName = invokeParams.Any(parameter => parameter.Name is "parameterName" or "paramName"); + + for (int index = 0; index < invokeParams.Length; index++) { + ParameterInfo classify = classifyParams[index]; + if (!IsGuardTarget(classify)) { continue; } + + string description = Describe(member, classify); + + object?[] arguments; + object? instance; + try { + instance = member.Invoke.IsStatic || member.Invoke is ConstructorInfo ? null : Sample(member.Receiver); + arguments = new object?[invokeParams.Length]; + for (int j = 0; j < invokeParams.Length; j++) { + arguments[j] = j == index ? null : ArgumentFor(invokeParams[j]); + } + } catch (Exception build) { + uncovered.Add($"{description}: could not build arguments — {Root(build).Message}"); + continue; + } + + try { + if (member.Invoke is ConstructorInfo ctor) { ctor.Invoke(arguments); } else { member.Invoke.Invoke(instance, arguments); } + violations.Add($"{description}: expected ArgumentNullException, nothing was thrown"); + } catch (TargetInvocationException invocation) { + Exception? thrown = invocation.InnerException; + if (thrown is ArgumentNullException guard && (guard.ParamName == classify.Name || forwardsName)) { continue; } + + string got = thrown is ArgumentNullException other + ? $"ArgumentNullException(ParamName=\"{other.ParamName}\")" + : thrown?.GetType().Name ?? "null"; + violations.Add($"{description}: expected ArgumentNullException(\"{classify.Name}\") but got {got}"); + } catch (Exception unexpected) { + uncovered.Add($"{description}: invocation failed — {Root(unexpected).GetType().Name}: {Root(unexpected).Message}"); + } + } + } + + // Parameters to read annotations and generic constraints from: for a generic method, the open definition (which + // still carries `T` and its constraints); for a member of a constructed generic type, the definition member + // (whose parameters carry the nullable annotations the constructed copy elides). + private static ParameterInfo[] ClassificationParameters(MemberDescriptor member) { + if (member.Classify is MethodInfo { IsGenericMethodDefinition: true } definition) { return definition.GetParameters(); } + + Type? declaring = member.Invoke.DeclaringType; + if (declaring is { IsGenericType: true, IsGenericTypeDefinition: false }) { + try { + Type openDeclaring = declaring.GetGenericTypeDefinition(); + if (openDeclaring.Module.ResolveMember(member.Invoke.MetadataToken) is MethodBase open) { return open.GetParameters(); } + } catch { + // fall through to the constructed parameters + } + } + + return member.Invoke.GetParameters(); + } + + private static bool IsGuardTarget(ParameterInfo parameter) { + Type type = parameter.ParameterType; + if (type.IsByRef) { return false; } // out/ref/in + + if (type.IsGenericParameter) { + return (type.GenericParameterAttributes & GenericParameterAttributes.ReferenceTypeConstraint) != 0; + } + + if (type.IsValueType || type.IsPointer) { return false; } // includes Nullable, enums, structs + + try { + return Nullability.Create(parameter).ReadState == NullabilityState.NotNull; + } catch { + return true; // unknown annotation → treat as a target so any gap shows up as a visible violation + } + } + + private static string Describe(MemberDescriptor member, ParameterInfo parameter) { + string kind = member.Invoke is ConstructorInfo ? "ctor" : member.Invoke.Name; + string signature = string.Join(", ", member.Invoke.GetParameters().Select(p => $"{Readable(p.ParameterType)} {p.Name}")); + + return $"{Readable(member.Receiver)}.{kind}({signature}) [param '{parameter.Name}']"; + } + + #endregion + + #region Sample values + + private static object? ArgumentFor(ParameterInfo parameter) { + Type type = parameter.ParameterType; + if (type.IsByRef) { type = type.GetElementType()!; } + + if (type.IsValueType) { + return Nullable.GetUnderlyingType(type) is not null ? null : Activator.CreateInstance(type); + } + + // A nullable non-target reference can simply be null; a non-nullable one needs a real, valid value so the + // member reaches (and only reaches) the guard of the parameter under test. + try { + if (Nullability.Create(parameter).ReadState == NullabilityState.Nullable) { return null; } + } catch { + // fall through and build a value + } + + return Sample(type); + } + + private static object Sample(Type type) { + foreach (object candidate in Samples) { + if (type.IsInstanceOfType(candidate)) { return candidate; } + } + + if (type == typeof(string)) { return "sample"; } + + if (type.IsArray) { + Type element = type.GetElementType()!; + Array array = Array.CreateInstance(element, 1); + array.SetValue(Element(element), 0); + + return array; + } + + if (type.IsGenericType) { + Type definition = type.GetGenericTypeDefinition(); + Type[] arguments = type.GetGenericArguments(); + + if (definition == typeof(IAny<>)) { return CreateAny(arguments[0]); } + + if (definition == typeof(IEnumerable<>) || definition == typeof(IReadOnlyList<>) || definition == typeof(IReadOnlyCollection<>) + || definition == typeof(IList<>) || definition == typeof(ICollection<>) || definition == typeof(List<>)) { + Type listType = typeof(List<>).MakeGenericType(arguments[0]); + var list = (IList)Activator.CreateInstance(listType)!; + list.Add(Element(arguments[0])); + + return list; + } + + if (definition == typeof(IReadOnlyDictionary<,>) || definition == typeof(IDictionary<,>) || definition == typeof(Dictionary<,>)) { + Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(arguments); + var dictionary = (IDictionary)Activator.CreateInstance(dictionaryType)!; + dictionary[Element(arguments[0])!] = Element(arguments[1]); + + return dictionary; + } + } + + if (typeof(Delegate).IsAssignableFrom(type)) { return CreateDelegate(type); } + + throw new NotSupportedException($"no sample available for {Readable(type)}"); + } + + private static object? Element(Type type) { + if (type == typeof(string)) { return "x"; } + if (type.IsValueType) { return Nullable.GetUnderlyingType(type) is not null ? null : Activator.CreateInstance(type); } + + return Sample(type); + } + + private static object CreateAny(Type valueType) { + MethodInfo oneOf = typeof(Any).GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(method => method is { Name: "OneOf", IsGenericMethodDefinition: true } + && method.GetParameters() is [{ ParameterType.IsArray: true }]) + .MakeGenericMethod(valueType); + + Array pool = Array.CreateInstance(valueType, 1); + pool.SetValue(Element(valueType), 0); + + return oneOf.Invoke(null, [pool])!; + } + + private static object CreateDelegate(Type delegateType) { + MethodInfo signature = delegateType.GetMethod("Invoke")!; + ParameterExpression[] inputs = signature.GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray(); + Type returnType = signature.ReturnType; + + Expression body = returnType == typeof(void) + ? Expression.Empty() + : Expression.Constant(Element(returnType), returnType); + + return Expression.Lambda(delegateType, body, inputs).Compile(); + } + + // A pool of live, valid internal instances (random sources, interval/string/uri specs, collection state, regex + // nodes, ...) harvested by walking the object graph of a handful of real generators. Reusing what the library + // itself builds is what lets the harness supply a valid `spec` when testing a `source` guard, without wiring up + // each engine type by hand. + private static List HarvestSamples() { + List pool = []; + HashSet visited = new(ReferenceEqualityComparer.Instance); + Queue frontier = new(); + + void Seed(object? root) { + if (root is null or string) { return; } + if (root.GetType().IsValueType) { return; } // primitives, enums, structs are never harvested samples + if (visited.Add(root)) { frontier.Enqueue(root); } + } + + AnyContext context = new(0); + Seed(context); + Seed(new FixedRandomSource(0)); + Seed(new SeededRandom(0)); + + foreach (MethodInfo factory in typeof(AnyContext).GetMethods(BindingFlags.Public | BindingFlags.Instance)) { + if (factory.DeclaringType == typeof(object)) { continue; } + if (factory.IsSpecialName || factory.GetParameters().Length != 0) { continue; } + if (factory.ReturnType == typeof(void) || factory.ReturnType == typeof(AnyContext) || factory.IsGenericMethodDefinition) { continue; } + try { Seed(factory.Invoke(context, null)); } catch { /* best effort */ } + } + + IAny strings = Any.String(); + void Try(Func build) { + try { Seed(build()); } catch { /* best effort */ } + } + + // Collections closed on the representative element type the harness uses (string), plus a dictionary and an enum. + Try(() => Any.ListOf(strings)); + Try(() => Any.SetOf(strings)); + Try(() => Any.SequenceOf(strings)); + Try(() => Any.ArrayOf(strings)); + Try(() => Any.DictionaryOf(strings, strings)); + Try(() => Any.OneOf("a", "b", "c")); + Try(() => Any.Enum()); + Try(() => strings.As(value => value.Length)); + Try(() => strings.OrNull()); + + // One pattern per regex-node shape, so every RegexNode subtype is reachable through AnyPattern's tree. + Try(() => Any.StringMatching("abc")); // sequence + Try(() => Any.StringMatching("a|b|c")); // alternation + Try(() => Any.StringMatching("a{2,3}")); // repeat + Try(() => Any.StringMatching("[a-z]")); // characters + + int budget = 0; + while (frontier.Count > 0 && budget++ < 5_000) { + object current = frontier.Dequeue(); + pool.Add(current); + + for (Type? level = current.GetType(); level is not null && level != typeof(object); level = level.BaseType) { + foreach (FieldInfo field in level.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { + Seed(field.GetValue(current)); + } + } + + // Follow parameterless generator-producing steps too, so a sibling generator reachable only through a + // fluent call (e.g. the concrete URI generators returned by AnyUri) still enters the pool. + if (current.GetType().Assembly != LibraryAssembly) { continue; } + foreach (MethodInfo step in current.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) { + if (step.DeclaringType == typeof(object) || step.IsSpecialName || step.IsGenericMethodDefinition) { continue; } + if (step.GetParameters().Length != 0 || step.ReturnType.Assembly != LibraryAssembly) { continue; } + try { Seed(step.Invoke(current, null)); } catch { /* best effort */ } + } + } + + return pool; + } + + #endregion + + #region Helpers + + private static bool TryClose(Type definition, out Type? closed) { + try { + closed = definition.MakeGenericType(definition.GetGenericArguments().Select(Representative).ToArray()); + + return true; + } catch { + closed = null; + + return false; + } + } + + private static bool TryClose(MethodInfo definition, out MethodInfo? closed) { + try { + closed = definition.MakeGenericMethod(definition.GetGenericArguments().Select(Representative).ToArray()); + + return true; + } catch { + closed = null; + + return false; + } + } + + private static Type Representative(Type parameter) { + GenericParameterAttributes attributes = parameter.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask; + Type[] constraints = parameter.GetGenericParameterConstraints(); + + if ((attributes & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0) { + return constraints.Any(constraint => constraint == typeof(Enum) || constraint.BaseType == typeof(Enum)) ? typeof(DayOfWeek) : typeof(int); + } + + Type? baseConstraint = constraints.FirstOrDefault(constraint => constraint is { IsClass: true } && constraint != typeof(object)); + if (baseConstraint is not null) { return baseConstraint; } + + return constraints.All(constraint => !constraint.IsInterface || constraint.IsAssignableFrom(typeof(string))) ? typeof(string) : typeof(int); + } + + private static Exception Root(Exception exception) { + return exception is TargetInvocationException { InnerException: { } inner } ? inner : exception; + } + + private static string Readable(Type type) { + if (!type.IsGenericType) { return type.Name; } + + string name = type.Name; + int tick = name.IndexOf('`'); + if (tick >= 0) { name = name[..tick]; } + + return $"{name}<{string.Join(", ", type.GetGenericArguments().Select(Readable))}>"; + } + + #endregion + +} diff --git a/JustDummies/Any.cs b/JustDummies/Any.cs index c509326f..5fed7a66 100644 --- a/JustDummies/Any.cs +++ b/JustDummies/Any.cs @@ -550,6 +550,8 @@ public static void Reproducibly(int seed, Action body, Action? report = /// A task that completes when completes, and faults with the body's exception. /// Thrown when is null. public static Task ReproduciblyAsync(Func body, Action? report = null) { + if (body is null) { throw new ArgumentNullException(nameof(body)); } + return ReproduciblyAsync(AmbientRandomSource.NewSeed(), body, report); } @@ -562,9 +564,15 @@ public static Task ReproduciblyAsync(Func body, Action? report = n /// The sink the seed is written to on failure. Defaults to when null. /// A task that completes when completes. /// Thrown when is null. - public static async Task ReproduciblyAsync(int seed, Func body, Action? report = null) { + public static Task ReproduciblyAsync(int seed, Func body, Action? report = null) { if (body is null) { throw new ArgumentNullException(nameof(body)); } + return RunReproduciblyAsync(seed, body, report); + } + + // Kept separate from the public entry so the null-argument guard above throws synchronously at the call site, + // rather than being deferred into the returned task's fault — which a caller who forgets to await would miss. + private static async Task RunReproduciblyAsync(int seed, Func body, Action? report) { using (AmbientRandomSource.UseSeed(seed)) { try { await body().ConfigureAwait(false); diff --git a/JustDummies/AnyArray.cs b/JustDummies/AnyArray.cs index 2d25dce4..e63a9d75 100644 --- a/JustDummies/AnyArray.cs +++ b/JustDummies/AnyArray.cs @@ -29,10 +29,14 @@ public AnyArray Distinct(IEqualityComparer comparer) { } private protected override AnyArray With(CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + return new AnyArray(SourceOrNull, state); } private protected override T[] Build(List items) { + if (items is null) { throw new ArgumentNullException(nameof(items)); } + return items.ToArray(); } diff --git a/JustDummies/AnyBoolean.cs b/JustDummies/AnyBoolean.cs index 8ffd354c..86f14e1e 100644 --- a/JustDummies/AnyBoolean.cs +++ b/JustDummies/AnyBoolean.cs @@ -10,6 +10,8 @@ public sealed class AnyBoolean : IAny, IHasRandomSource, ICardinalityHint< #region Statics members declarations internal static AnyBoolean Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyBoolean(source, null, null); } diff --git a/JustDummies/AnyByte.cs b/JustDummies/AnyByte.cs index b768a197..0158e253 100644 --- a/JustDummies/AnyByte.cs +++ b/JustDummies/AnyByte.cs @@ -17,6 +17,8 @@ public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint V(Val(ordinal)), Ord(byte.MinValue), Ord(byte.MaxValue))); } diff --git a/JustDummies/AnyChar.cs b/JustDummies/AnyChar.cs index 71879f4a..e0b8ae35 100644 --- a/JustDummies/AnyChar.cs +++ b/JustDummies/AnyChar.cs @@ -13,6 +13,8 @@ public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint : IAny, IHas #endregion private protected AnyCollection(RandomSource? source, CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + SourceOrNull = source; State = state; } diff --git a/JustDummies/AnyDateOnly.cs b/JustDummies/AnyDateOnly.cs index 42187a24..bfcb8caa 100644 --- a/JustDummies/AnyDateOnly.cs +++ b/JustDummies/AnyDateOnly.cs @@ -21,6 +21,8 @@ public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinality #region Statics members declarations internal static AnyDateOnly Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyDateOnly(source, OrdinalIntervalSpec.Unconstrained("DateOnly", ordinal => V(Val(ordinal)), Ord(DateOnly.MinValue), Ord(DateOnly.MaxValue))); } diff --git a/JustDummies/AnyDateTime.cs b/JustDummies/AnyDateTime.cs index 5b2c6fc9..e55e6c58 100644 --- a/JustDummies/AnyDateTime.cs +++ b/JustDummies/AnyDateTime.cs @@ -24,6 +24,8 @@ public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinality #region Statics members declarations internal static AnyDateTime Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyDateTime(source, OrdinalIntervalSpec.Unconstrained("DateTime", ordinal => V(Val(ordinal)), Ord(DateTime.MinValue), Ord(DateTime.MaxValue))); } diff --git a/JustDummies/AnyDateTimeOffset.cs b/JustDummies/AnyDateTimeOffset.cs index fe64034c..83f023b2 100644 --- a/JustDummies/AnyDateTimeOffset.cs +++ b/JustDummies/AnyDateTimeOffset.cs @@ -30,6 +30,8 @@ public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, #region Statics members declarations internal static AnyDateTimeOffset Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyDateTimeOffset(source, OrdinalIntervalSpec.Unconstrained("DateTimeOffset", ordinal => V(Val(ordinal)), Ord(DateTimeOffset.MinValue), Ord(DateTimeOffset.MaxValue)), null, false, 0, 0); } diff --git a/JustDummies/AnyDecimal.cs b/JustDummies/AnyDecimal.cs index be693d62..436d4bda 100644 --- a/JustDummies/AnyDecimal.cs +++ b/JustDummies/AnyDecimal.cs @@ -18,6 +18,8 @@ public sealed class AnyDecimal : IAny, IHasRandomSource, ICardinalityHi #region Statics members declarations internal static AnyDecimal Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyDecimal(source, DecimalIntervalSpec.Unconstrained("Decimal", V)); } diff --git a/JustDummies/AnyDerivation.cs b/JustDummies/AnyDerivation.cs index 9f0ced3e..27ace873 100644 --- a/JustDummies/AnyDerivation.cs +++ b/JustDummies/AnyDerivation.cs @@ -25,6 +25,8 @@ internal sealed class DerivedAny : IAny, IHasRandomSource, IReproducibilit #endregion internal DerivedAny(RandomSource? source, bool drawsOnlyFromSource, Func generate) { + if (generate is null) { throw new ArgumentNullException(nameof(generate)); } + _source = source; _drawsOnlyFromSource = drawsOnlyFromSource; _generate = generate; @@ -46,6 +48,8 @@ internal static class AnyDerivation { /// The random context of , when it is one of the library's own. internal static RandomSource? SourceOf(IAny generator) { + if (generator is null) { throw new ArgumentNullException(nameof(generator)); } + return (generator as IHasRandomSource)?.Source; } @@ -58,6 +62,8 @@ internal static class AnyDerivation { /// replayed from that seed. /// internal static bool IsReproducible(IAny generator) { + if (generator is null) { throw new ArgumentNullException(nameof(generator)); } + if (generator is IReproducibilityHint hint) { return hint.DrawsOnlyFromSource; } return SourceOf(generator) is not null; @@ -68,6 +74,8 @@ internal static bool IsReproducible(IAny generator) { /// advertises one through ; null when the domain is unbounded or unknown. /// internal static long? CardinalityOf(IAny generator) { + if (generator is null) { throw new ArgumentNullException(nameof(generator)); } + return (generator as ICardinalityHint)?.DistinctCardinality; } @@ -79,6 +87,9 @@ internal static bool IsReproducible(IAny generator) { /// promising a full replay the seed cannot deliver. The library's own exceptions pass through untouched. /// internal static T Invoke(Func invoke, RandomSource? source, bool reproducible, string failure) { + if (invoke is null) { throw new ArgumentNullException(nameof(invoke)); } + if (failure is null) { throw new ArgumentNullException(nameof(failure)); } + try { return invoke(); } catch (AnyException) { diff --git a/JustDummies/AnyDictionary.cs b/JustDummies/AnyDictionary.cs index b96a8123..6a49ffda 100644 --- a/JustDummies/AnyDictionary.cs +++ b/JustDummies/AnyDictionary.cs @@ -28,7 +28,10 @@ public sealed class AnyDictionary : IAny> #endregion internal AnyDictionary(RandomSource? source, CollectionState keys, IAny values) - : this(source, keys, values, NoPinnedValues) { } + : this(source, keys, values, NoPinnedValues) { + if (keys is null) { throw new ArgumentNullException(nameof(keys)); } + if (values is null) { throw new ArgumentNullException(nameof(values)); } + } private AnyDictionary(RandomSource? source, CollectionState keys, IAny values, IReadOnlyDictionary pinnedValues) { _source = source; diff --git a/JustDummies/AnyDouble.cs b/JustDummies/AnyDouble.cs index 3cb12909..97821497 100644 --- a/JustDummies/AnyDouble.cs +++ b/JustDummies/AnyDouble.cs @@ -17,6 +17,8 @@ public sealed class AnyDouble : IAny, IHasRandomSource, ICardinalityHint #region Statics members declarations internal static AnyDouble Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyDouble(source, ContinuousIntervalSpec.Unconstrained("Double", V, value => value, ContinuousIntervalSpec.NextUp, -double.MaxValue, double.MaxValue)); } diff --git a/JustDummies/AnyEnum.cs b/JustDummies/AnyEnum.cs index f01189ba..36294c10 100644 --- a/JustDummies/AnyEnum.cs +++ b/JustDummies/AnyEnum.cs @@ -43,6 +43,7 @@ public sealed class AnyEnum : IAny, IHasRandomSource, ICardinality private static TEnum[]? _combinations; internal static AnyEnum Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } if (Declared.Length == 0) { throw new AnyGenerationException($"Cannot generate an arbitrary {typeof(TEnum).Name} value because the enum declares no members."); } diff --git a/JustDummies/AnyFtpUri.cs b/JustDummies/AnyFtpUri.cs index 280b4484..a6fd5bbd 100644 --- a/JustDummies/AnyFtpUri.cs +++ b/JustDummies/AnyFtpUri.cs @@ -14,6 +14,8 @@ public sealed class AnyFtpUri : IAny, IHasRandomSource { #endregion internal AnyFtpUri(RandomSource source, UriSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/AnyGuid.cs b/JustDummies/AnyGuid.cs index cd6926b5..d357e9f0 100644 --- a/JustDummies/AnyGuid.cs +++ b/JustDummies/AnyGuid.cs @@ -13,6 +13,8 @@ public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint, IHasRandomSource, ICardinalityHint V((Half)value), value => (double)(Half)value, value => NextUp((Half)value), -(double)Half.MaxValue, (double)Half.MaxValue)); } diff --git a/JustDummies/AnyInt128.cs b/JustDummies/AnyInt128.cs index a61d8b4e..7f60c353 100644 --- a/JustDummies/AnyInt128.cs +++ b/JustDummies/AnyInt128.cs @@ -19,6 +19,8 @@ public sealed class AnyInt128 : IAny, IHasRandomSource, ICardinalityHint #region Statics members declarations internal static AnyInt128 Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyInt128(source, WideIntervalSpec.Unconstrained("Int128", ordinal => V(Val(ordinal)), Ord(Int128.MinValue), Ord(Int128.MaxValue))); } diff --git a/JustDummies/AnyInt16.cs b/JustDummies/AnyInt16.cs index c1258911..31bede8f 100644 --- a/JustDummies/AnyInt16.cs +++ b/JustDummies/AnyInt16.cs @@ -17,6 +17,8 @@ public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint V(Val(ordinal)), Ord(short.MinValue), Ord(short.MaxValue))); } diff --git a/JustDummies/AnyInt32.cs b/JustDummies/AnyInt32.cs index 429c47e0..dcf308bd 100644 --- a/JustDummies/AnyInt32.cs +++ b/JustDummies/AnyInt32.cs @@ -33,6 +33,8 @@ public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint V(Val(ordinal)), Ord(int.MinValue), Ord(int.MaxValue))); } diff --git a/JustDummies/AnyInt64.cs b/JustDummies/AnyInt64.cs index 4549ceaf..8e0bf9a2 100644 --- a/JustDummies/AnyInt64.cs +++ b/JustDummies/AnyInt64.cs @@ -17,6 +17,8 @@ public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint V(Val(ordinal)), Ord(long.MinValue), Ord(long.MaxValue))); } diff --git a/JustDummies/AnyList.cs b/JustDummies/AnyList.cs index f71fe051..afd2dc76 100644 --- a/JustDummies/AnyList.cs +++ b/JustDummies/AnyList.cs @@ -29,10 +29,14 @@ public AnyList Distinct(IEqualityComparer comparer) { } private protected override AnyList With(CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + return new AnyList(SourceOrNull, state); } private protected override List Build(List items) { + if (items is null) { throw new ArgumentNullException(nameof(items)); } + return items; } diff --git a/JustDummies/AnyMailtoUri.cs b/JustDummies/AnyMailtoUri.cs index 35df59a5..09101b5f 100644 --- a/JustDummies/AnyMailtoUri.cs +++ b/JustDummies/AnyMailtoUri.cs @@ -16,6 +16,8 @@ public sealed class AnyMailtoUri : IAny, IHasRandomSource { #endregion internal AnyMailtoUri(RandomSource source, UriSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/AnyOneOf.cs b/JustDummies/AnyOneOf.cs index bc2ac711..dccbd155 100644 --- a/JustDummies/AnyOneOf.cs +++ b/JustDummies/AnyOneOf.cs @@ -34,9 +34,12 @@ public sealed class AnyOneOf : IAny, IHasRandomSource, ICardinalityHint #region Statics members declarations - // Validates and deduplicates the caller's pool, then builds the generator. The array-null check belongs to the - // public factories (they own the parameter name); by the time we get here the pool is non-null and materialized. + // Validates and deduplicates the caller's pool, then builds the generator. As an internal boundary it guards its + // own arguments per the null-argument convention (ADR-0045); the public factories additionally reject a null + // array first, under the caller-facing parameter name, before delegating here. internal static AnyOneOf FromPool(RandomSource source, IReadOnlyList values) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (values is null) { throw new ArgumentNullException(nameof(values)); } if (values.Count == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element; use OrNull() to make the whole generator nullable.", nameof(values)); } diff --git a/JustDummies/AnyPattern.cs b/JustDummies/AnyPattern.cs index e1429140..aba3dd13 100644 --- a/JustDummies/AnyPattern.cs +++ b/JustDummies/AnyPattern.cs @@ -36,6 +36,8 @@ public sealed class AnyPattern : IAny, IHasRandomSource { private const int GenerationLimit = 65536; internal static AnyPattern FromPattern(RandomSource source, string pattern, bool ignoreCase) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } return new AnyPattern(source, RegexParser.Parse(pattern, ignoreCase)); } @@ -49,6 +51,8 @@ internal static AnyPattern FromPattern(RandomSource source, string pattern, bool #endregion internal AnyPattern(RandomSource source, RegexNode root) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (root is null) { throw new ArgumentNullException(nameof(root)); } _source = source; _root = root; } diff --git a/JustDummies/AnyRelativeUri.cs b/JustDummies/AnyRelativeUri.cs index 4e66b182..ec8779b9 100644 --- a/JustDummies/AnyRelativeUri.cs +++ b/JustDummies/AnyRelativeUri.cs @@ -16,6 +16,8 @@ public sealed class AnyRelativeUri : IAny, IHasRandomSource { #endregion internal AnyRelativeUri(RandomSource source, UriSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/AnySByte.cs b/JustDummies/AnySByte.cs index 741a1462..250ee6be 100644 --- a/JustDummies/AnySByte.cs +++ b/JustDummies/AnySByte.cs @@ -17,6 +17,8 @@ public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint V(Val(ordinal)), Ord(sbyte.MinValue), Ord(sbyte.MaxValue))); } diff --git a/JustDummies/AnySequence.cs b/JustDummies/AnySequence.cs index b36588b5..28278e84 100644 --- a/JustDummies/AnySequence.cs +++ b/JustDummies/AnySequence.cs @@ -35,10 +35,14 @@ public AnySequence Distinct(IEqualityComparer comparer) { } private protected override AnySequence With(CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + return new AnySequence(SourceOrNull, state); } private protected override IEnumerable Build(List items) { + if (items is null) { throw new ArgumentNullException(nameof(items)); } + return items; } diff --git a/JustDummies/AnySet.cs b/JustDummies/AnySet.cs index a6fb0bd4..13cf3448 100644 --- a/JustDummies/AnySet.cs +++ b/JustDummies/AnySet.cs @@ -14,10 +14,14 @@ public sealed class AnySet : AnyCollection, AnySet> { internal AnySet(RandomSource? source, CollectionState state) : base(source, state) { } private protected override AnySet With(CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + return new AnySet(SourceOrNull, state); } private protected override HashSet Build(List items) { + if (items is null) { throw new ArgumentNullException(nameof(items)); } + // The state already deduplicated under the comparer; the set carries the same comparer so later lookups // by the caller behave identically. return new HashSet(items, State.Comparer); diff --git a/JustDummies/AnySingle.cs b/JustDummies/AnySingle.cs index 51050c7b..79ad4dfe 100644 --- a/JustDummies/AnySingle.cs +++ b/JustDummies/AnySingle.cs @@ -17,6 +17,8 @@ public sealed class AnySingle : IAny, IHasRandomSource, ICardinalityHint< #region Statics members declarations internal static AnySingle Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnySingle(source, ContinuousIntervalSpec.Unconstrained("Single", value => V((float)value), value => (float)value, value => NextUp((float)value), -float.MaxValue, float.MaxValue)); } diff --git a/JustDummies/AnyString.cs b/JustDummies/AnyString.cs index 671a5659..eab80a49 100644 --- a/JustDummies/AnyString.cs +++ b/JustDummies/AnyString.cs @@ -69,6 +69,8 @@ private static int RequireNonNegative(int length, string parameterName) { #endregion internal AnyString(RandomSource source, StringSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/AnyStringOneOf.cs b/JustDummies/AnyStringOneOf.cs index 8f09ec54..94f10b11 100644 --- a/JustDummies/AnyStringOneOf.cs +++ b/JustDummies/AnyStringOneOf.cs @@ -33,6 +33,8 @@ public sealed class AnyStringOneOf : IAny, IHasRandomSource, ICardinalit #endregion internal AnyStringOneOf(RandomSource source, IReadOnlyList values) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (values is null) { throw new ArgumentNullException(nameof(values)); } _source = source; _values = values; } diff --git a/JustDummies/AnyTimeOnly.cs b/JustDummies/AnyTimeOnly.cs index d8340de6..2fb8ac54 100644 --- a/JustDummies/AnyTimeOnly.cs +++ b/JustDummies/AnyTimeOnly.cs @@ -21,6 +21,8 @@ public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinality #region Statics members declarations internal static AnyTimeOnly Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyTimeOnly(source, OrdinalIntervalSpec.Unconstrained("TimeOnly", ordinal => V(Val(ordinal)), Ord(TimeOnly.MinValue), Ord(TimeOnly.MaxValue))); } diff --git a/JustDummies/AnyTimeSpan.cs b/JustDummies/AnyTimeSpan.cs index 1514fba7..f5460701 100644 --- a/JustDummies/AnyTimeSpan.cs +++ b/JustDummies/AnyTimeSpan.cs @@ -18,6 +18,8 @@ public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinality #region Statics members declarations internal static AnyTimeSpan Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyTimeSpan(source, OrdinalIntervalSpec.Unconstrained("TimeSpan", ordinal => V(Val(ordinal)), Ord(TimeSpan.MinValue), Ord(TimeSpan.MaxValue))); } diff --git a/JustDummies/AnyUInt128.cs b/JustDummies/AnyUInt128.cs index 7aa25df5..f3c32af0 100644 --- a/JustDummies/AnyUInt128.cs +++ b/JustDummies/AnyUInt128.cs @@ -19,6 +19,8 @@ public sealed class AnyUInt128 : IAny, IHasRandomSource, ICardinalityHi #region Statics members declarations internal static AnyUInt128 Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyUInt128(source, WideIntervalSpec.Unconstrained("UInt128", ordinal => V(Val(ordinal)), Ord(UInt128.MinValue), Ord(UInt128.MaxValue))); } diff --git a/JustDummies/AnyUInt16.cs b/JustDummies/AnyUInt16.cs index a31e59d7..8e3de031 100644 --- a/JustDummies/AnyUInt16.cs +++ b/JustDummies/AnyUInt16.cs @@ -17,6 +17,8 @@ public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint #region Statics members declarations internal static AnyUInt16 Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyUInt16(source, OrdinalIntervalSpec.Unconstrained("UInt16", ordinal => V(Val(ordinal)), Ord(ushort.MinValue), Ord(ushort.MaxValue))); } diff --git a/JustDummies/AnyUInt32.cs b/JustDummies/AnyUInt32.cs index 2cd9c84b..20397e4e 100644 --- a/JustDummies/AnyUInt32.cs +++ b/JustDummies/AnyUInt32.cs @@ -17,6 +17,8 @@ public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint V(Val(ordinal)), Ord(uint.MinValue), Ord(uint.MaxValue))); } diff --git a/JustDummies/AnyUInt64.cs b/JustDummies/AnyUInt64.cs index 20a25a15..010c19f5 100644 --- a/JustDummies/AnyUInt64.cs +++ b/JustDummies/AnyUInt64.cs @@ -17,6 +17,8 @@ public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint< #region Statics members declarations internal static AnyUInt64 Create(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + return new AnyUInt64(source, OrdinalIntervalSpec.Unconstrained("UInt64", ordinal => V(Val(ordinal)), Ord(ulong.MinValue), Ord(ulong.MaxValue))); } diff --git a/JustDummies/AnyUri.cs b/JustDummies/AnyUri.cs index a712cb74..ab576cc6 100644 --- a/JustDummies/AnyUri.cs +++ b/JustDummies/AnyUri.cs @@ -35,6 +35,8 @@ public sealed class AnyUri : IAny, IHasRandomSource { #endregion internal AnyUri(RandomSource source, UriSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/AnyWebSocketUri.cs b/JustDummies/AnyWebSocketUri.cs index 011f0950..dabb5653 100644 --- a/JustDummies/AnyWebSocketUri.cs +++ b/JustDummies/AnyWebSocketUri.cs @@ -15,6 +15,8 @@ public sealed class AnyWebSocketUri : IAny, IHasRandomSource { #endregion internal AnyWebSocketUri(RandomSource source, UriSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/AnyWebUri.cs b/JustDummies/AnyWebUri.cs index 82d3ed5b..32cf8658 100644 --- a/JustDummies/AnyWebUri.cs +++ b/JustDummies/AnyWebUri.cs @@ -16,6 +16,8 @@ public sealed class AnyWebUri : IAny, IHasRandomSource { #endregion internal AnyWebUri(RandomSource source, UriSpec spec) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + if (spec is null) { throw new ArgumentNullException(nameof(spec)); } _source = source; _spec = spec; } diff --git a/JustDummies/CollectionState.cs b/JustDummies/CollectionState.cs index 5284aa1f..56ff34e6 100644 --- a/JustDummies/CollectionState.cs +++ b/JustDummies/CollectionState.cs @@ -29,6 +29,8 @@ internal sealed class CollectionState { #region Statics members declarations internal static CollectionState Create(IAny item, bool distinct, IEqualityComparer? comparer) { + if (item is null) { throw new ArgumentNullException(nameof(item)); } + return new CollectionState(item, AnyDerivation.CardinalityOf(item), CountSpec.Unconstrained, distinct, comparer, Array.Empty(), Array.Empty>()); } @@ -83,36 +85,51 @@ private CollectionState(IAny item, long? itemCardinality, CountSpec count, bo /// Fixes the exact element count. internal CollectionState WithExactCount(int count, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return Rebuild(_count.WithExactCount(count, applying), _distinct, _comparer, _fixedContaining, _generatedContaining, applying); } /// Tightens the minimum element count. internal CollectionState WithMinCount(int count, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return Rebuild(_count.WithMinCount(count, applying), _distinct, _comparer, _fixedContaining, _generatedContaining, applying); } /// Tightens the maximum element count. internal CollectionState WithMaxCount(int count, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return Rebuild(_count.WithMaxCount(count, applying), _distinct, _comparer, _fixedContaining, _generatedContaining, applying); } /// Requires the elements to be pairwise distinct, optionally under . internal CollectionState AsDistinct(IEqualityComparer? comparer, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return Rebuild(_count, true, comparer ?? _comparer, _fixedContaining, _generatedContaining, applying); } /// Requires the collection to contain . internal CollectionState WithContaining(T value, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return Rebuild(_count, _distinct, _comparer, Append(_fixedContaining, value), _generatedContaining, applying); } /// Requires the collection to contain a value drawn from . internal CollectionState WithContaining(IAny generator, string applying) { + if (generator is null) { throw new ArgumentNullException(nameof(generator)); } + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return Rebuild(_count, _distinct, _comparer, _fixedContaining, Append(_generatedContaining, generator), applying); } /// Builds one collection satisfying the whole specification — laid out directly, never generate-then-retry. internal List Materialize(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + SeededRandom random = source.Current; int required = _fixedContaining.Count + _generatedContaining.Count; int? cap = _distinct ? CardinalityCap() : null; diff --git a/JustDummies/ContinuousIntervalSpec.cs b/JustDummies/ContinuousIntervalSpec.cs index 13a51048..b1ffff70 100644 --- a/JustDummies/ContinuousIntervalSpec.cs +++ b/JustDummies/ContinuousIntervalSpec.cs @@ -28,11 +28,17 @@ internal sealed class ContinuousIntervalSpec { #region Statics members declarations internal static ContinuousIntervalSpec Unconstrained(string typeName, Func render, Func quantize, Func nextUp, double domainMin, double domainMax) { + if (typeName is null) { throw new ArgumentNullException(nameof(typeName)); } + if (render is null) { throw new ArgumentNullException(nameof(render)); } + if (quantize is null) { throw new ArgumentNullException(nameof(quantize)); } + if (nextUp is null) { throw new ArgumentNullException(nameof(nextUp)); } + return new ContinuousIntervalSpec(typeName, render, quantize, nextUp, domainMin, null, domainMax, null, null, null, []); } /// Rejects NaN and the infinities — the shared argument guard of every floating-point generator. internal static void EnsureFinite(double value, string parameterName) { + if (parameterName is null) { throw new ArgumentNullException(nameof(parameterName)); } if (double.IsNaN(value) || double.IsInfinity(value)) { throw new ArgumentException("The value must be finite: NaN and infinities are never generated.", parameterName); } } @@ -93,6 +99,7 @@ private ContinuousIntervalSpec(string typeName, Func render, Fu /// Tightens the lower bound; a looser bound than the current one is a no-op. internal ContinuousIntervalSpec WithMinimum(double minimum, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (double.IsInfinity(minimum)) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } if (minimum <= _min) { return this; } @@ -107,6 +114,7 @@ internal ContinuousIntervalSpec WithMinimum(double minimum, string applying) { /// Tightens the upper bound; a looser bound than the current one is a no-op. internal ContinuousIntervalSpec WithMaximum(double maximum, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (double.IsInfinity(maximum)) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } if (maximum >= _max) { return this; } @@ -121,16 +129,22 @@ internal ContinuousIntervalSpec WithMaximum(double maximum, string applying) { /// Tightens the lower bound to strictly above — via the type's next representable value. internal ContinuousIntervalSpec WithMinimumAbove(double bound, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return WithMinimum(_nextUp(bound), applying); } /// Tightens the upper bound to strictly below — via the type's next representable value. internal ContinuousIntervalSpec WithMaximumBelow(double bound, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return WithMaximum(-_nextUp(-bound), applying); } /// Restricts the domain to an explicit allow-list; declared once per generator. internal ContinuousIntervalSpec WithAllowed(double[] values, string applying) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } double[] distinct = values.Distinct().ToArray(); @@ -140,6 +154,9 @@ internal ContinuousIntervalSpec WithAllowed(double[] values, string applying) { /// Adds values the generator must never produce. internal ContinuousIntervalSpec WithExcluded(double[] values, string applying) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + // The applied constraint tags its own values, so a later exhaustion message can name the exclusion // that actually emptied the domain rather than a bound that merely happens to border it. List<(string Constraint, double[] Ordinals)> exclusions = new(_exclusions) { (applying, values) }; @@ -176,6 +193,8 @@ internal bool Contains(double value) { /// Draws one value satisfying the whole specification. internal double Generate(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + SeededRandom random = source.Current; if (_effectiveAllowed is not null) { diff --git a/JustDummies/CountConstraints.cs b/JustDummies/CountConstraints.cs index c17aa1c6..817678a0 100644 --- a/JustDummies/CountConstraints.cs +++ b/JustDummies/CountConstraints.cs @@ -39,16 +39,21 @@ private static int RequireNonNegative(int count, string parameterName) { /// Requires at least one element. internal static CollectionState NonEmpty(CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + return state.WithMinCount(1, "NonEmpty()"); } /// Fixes the collection to no elements. internal static CollectionState Empty(CollectionState state) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } + return state.WithExactCount(0, "Empty()"); } /// Fixes the exact number of elements. internal static CollectionState WithCount(CollectionState state, int count) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } RequireNonNegative(count, nameof(count)); return state.WithExactCount(count, $"WithCount({V(count)})"); @@ -56,6 +61,7 @@ internal static CollectionState WithCount(CollectionState state, int co /// Requires at least elements. internal static CollectionState WithMinCount(CollectionState state, int count) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } RequireNonNegative(count, nameof(count)); return state.WithMinCount(count, $"WithMinCount({V(count)})"); @@ -63,6 +69,7 @@ internal static CollectionState WithMinCount(CollectionState state, int /// Requires at most elements. internal static CollectionState WithMaxCount(CollectionState state, int count) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } RequireNonNegative(count, nameof(count)); return state.WithMaxCount(count, $"WithMaxCount({V(count)})"); @@ -70,6 +77,7 @@ internal static CollectionState WithMaxCount(CollectionState state, int /// Requires a number of elements within the inclusive range [, ]. internal static CollectionState WithCountBetween(CollectionState state, int minimum, int maximum) { + if (state is null) { throw new ArgumentNullException(nameof(state)); } RequireNonNegative(minimum, nameof(minimum)); RequireNonNegative(maximum, nameof(maximum)); if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } diff --git a/JustDummies/CountSpec.cs b/JustDummies/CountSpec.cs index dc7fa1a2..dfb01ac2 100644 --- a/JustDummies/CountSpec.cs +++ b/JustDummies/CountSpec.cs @@ -69,6 +69,7 @@ private CountSpec(int? exact, string? exactConstraint, /// Fixes the exact count; declared once per generator. internal CountSpec WithExactCount(int count, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (_exactConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_exactConstraint} is already defined."); } return new CountSpec(count, applying, _min, _minConstraint, _max, _maxConstraint).Validated(applying); @@ -76,6 +77,7 @@ internal CountSpec WithExactCount(int count, string applying) { /// Tightens the minimum count; a looser bound than the current one is a no-op. internal CountSpec WithMinCount(int count, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (count <= _min) { return this; } return new CountSpec(_exact, _exactConstraint, count, applying, _max, _maxConstraint).Validated(applying); @@ -83,6 +85,7 @@ internal CountSpec WithMinCount(int count, string applying) { /// Tightens the maximum count; a looser bound than the current one is a no-op. internal CountSpec WithMaxCount(int count, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (_max is not null && count >= _max) { return this; } return new CountSpec(_exact, _exactConstraint, _min, _minConstraint, count, applying).Validated(applying); @@ -95,6 +98,7 @@ internal CountSpec WithMaxCount(int count, string applying) { /// compatible with the declared bounds — the collection validates them eagerly before generation. /// internal int Resolve(SeededRandom random, int requiredMin, int? cap) { + if (random is null) { throw new ArgumentNullException(nameof(random)); } if (_exact is int exact) { return exact; } int min = Math.Max(_min, requiredMin); @@ -112,6 +116,7 @@ internal int Resolve(SeededRandom random, int requiredMin, int? cap) { /// was the count cap or the containment requirement. /// internal void EnsureFits(int required, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } int? cap = _exact ?? _max; if (cap is int ceiling && required > ceiling) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {Elements(required)} required to be contained cannot fit in a collection of at most {Elements(ceiling)}."); diff --git a/JustDummies/DecimalIntervalSpec.cs b/JustDummies/DecimalIntervalSpec.cs index a528e095..e5472a05 100644 --- a/JustDummies/DecimalIntervalSpec.cs +++ b/JustDummies/DecimalIntervalSpec.cs @@ -20,6 +20,9 @@ internal sealed class DecimalIntervalSpec { #region Statics members declarations internal static DecimalIntervalSpec Unconstrained(string typeName, Func render) { + if (typeName is null) { throw new ArgumentNullException(nameof(typeName)); } + if (render is null) { throw new ArgumentNullException(nameof(render)); } + return new DecimalIntervalSpec(typeName, render, decimal.MinValue, null, decimal.MaxValue, null, null, null, [], NoScale, null); } @@ -96,6 +99,7 @@ private DecimalIntervalSpec(string typeName, Func render, /// Tightens the lower bound; a looser bound than the current one is a no-op. internal DecimalIntervalSpec WithMinimum(decimal minimum, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (minimum <= _min) { return this; } if (minimum > _max) { @@ -109,6 +113,7 @@ internal DecimalIntervalSpec WithMinimum(decimal minimum, string applying) { /// Tightens the upper bound; a looser bound than the current one is a no-op. internal DecimalIntervalSpec WithMaximum(decimal maximum, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (maximum >= _max) { return this; } if (maximum < _min) { @@ -122,16 +127,22 @@ internal DecimalIntervalSpec WithMaximum(decimal maximum, string applying) { /// Tightens the lower bound to strictly above — the inclusive bound plus a point exclusion. internal DecimalIntervalSpec WithMinimumAbove(decimal bound, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return WithMinimum(bound, applying).WithExcluded([bound], applying); } /// Tightens the upper bound to strictly below — the inclusive bound plus a point exclusion. internal DecimalIntervalSpec WithMaximumBelow(decimal bound, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + return WithMaximum(bound, applying).WithExcluded([bound], applying); } /// Restricts the domain to an explicit allow-list; declared once per generator. internal DecimalIntervalSpec WithAllowed(decimal[] values, string applying) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } decimal[] distinct = values.Distinct().ToArray(); @@ -141,6 +152,9 @@ internal DecimalIntervalSpec WithAllowed(decimal[] values, string applying) { /// Adds values the generator must never produce. internal DecimalIntervalSpec WithExcluded(decimal[] values, string applying) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } + // The applied constraint tags its own values, so a later exhaustion message can name the exclusion // that actually emptied the domain rather than a bound that merely happens to border it. List<(string Constraint, decimal[] Ordinals)> exclusions = new(_exclusions) { (applying, values) }; @@ -154,6 +168,7 @@ internal DecimalIntervalSpec WithExcluded(decimal[] values, string applying) { /// rendered form is not padded with trailing zeros. Declared once per generator. /// internal DecimalIntervalSpec WithScale(int scale, string applying) { + if (applying is null) { throw new ArgumentNullException(nameof(applying)); } if (_scale >= 0) { if (_scale == scale) { return this; } @@ -198,6 +213,8 @@ internal bool Contains(decimal value) { /// Draws one value satisfying the whole specification. internal decimal Generate(RandomSource source) { + if (source is null) { throw new ArgumentNullException(nameof(source)); } + SeededRandom random = source.Current; if (_effectiveAllowed is not null) { diff --git a/JustDummies/JustDummies.csproj b/JustDummies/JustDummies.csproj index 3cf890e1..986cf99c 100644 --- a/JustDummies/JustDummies.csproj +++ b/JustDummies/JustDummies.csproj @@ -57,6 +57,14 @@ + + + + +