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 @@ + + + + +