JustDummies

Just dummies — but seriously powerful ones.

Documentation sections

JD032: BoundDeclaredTwice

PropertyValue
CategoryConstraints (JustDummies.Constraints)
Severity🟠 Warning
Enabled by defaultYes

A chain declares the same bound twice, and only the tighter of the two survives.

Bounds fold silently and monotonically: a minimum keeps the larger of the two values, a maximum the smaller, and the losing call returns the generator unchanged. Nothing throws, and no run-time report mentions it. So one of the two calls is always dead — the looser one, whichever order you wrote them in.

Noncompliant

string reference = Any.String().WithMinLength(8).WithMinLength(10).Generate();  // draws ≥ 10; the 8 is erased
int    quantity  = Any.Int32().LessThanOrEqualTo(50).LessThanOrEqualTo(90).Generate();  // draws ≤ 50; the 90 is inert

Compliant

string reference = Any.String().WithMinLength(10).Generate();
int    quantity  = Any.Int32().LessThanOrEqualTo(50).Generate();

Keep the bound you meant and delete the other. If you meant a range, name it: WithLengthBetween(10, 20), Between(1, 50) — see JD031.

The four vocabularies, both orders

bound declared twicegenerators
WithMinLength, WithMaxLengthAnyString
WithMinCount, WithMaxCountevery collection generator
GreaterThanOrEqualTo, LessThanOrEqualTo, GreaterThan, LessThanthe numeric generators and TimeSpan
AfterOrEqualTo, BeforeOrEqualTo, After, BeforeAnyDateOnly, AnyDateTime, AnyDateTimeOffset, AnyTimeOnly

Strict bounds are reported here, where JD031 refuses them. JD031 has to name a replacement call and cannot do so for a strict pair without rewriting your numbers; this rule names no replacement, so GreaterThan(1).GreaterThan(5) is as reportable as any other duplicate.

The diagnostic lands on the second declaration — the call that made the chain ambiguous, and the one whose removal you are weighing. It deliberately does not say which of the two survives: that is whichever is tighter, and answering it would mean evaluating arguments this rule never reads, on types like TimeSpan and DateTime where no compile-time constant can represent them.

Why a warning, where JD024 is information

The precedent is JD025, not JD024. A value listed twice in a pool collapses silently, nothing throws, the domain stays well defined — and it is a warning, because the surface reads bigger than it is and the gap surfaces far from where it was written. A bound declared twice is the same shape.

JD024 sits at information for a reason that does not transfer: an inert constraint has a defensible reading, since a team can exclude a sentinel before the range that would produce it exists, so the constraint documents an intent that survives a future widening. A bound written twice inside one expression has no such reading. Both calls are in front of the same reader, the tighter simply erases the looser, and there is no future in which the erased call starts mattering.

What it does not flag

Bounds held under a name. This is the important one, and it is soundness rather than scope:

AnyString atLeastEight = Any.String().WithMinLength(8);
AnyString atLeastTen   = atLeastEight.WithMinLength(10);

A generator is an immutable recipe, so atLeastEight still exists and still draws a minimum of eight. It is a legitimate generator in its own right, nothing about WithMinLength(8) is dead, and reporting it would be a false positive. What makes the looser call dead inside a single chain is that the intermediate generator is unnamed and unreachable — the moment it is bound to a name, that stops being true. The rule therefore never follows a generator through a local, a field, a parameter or a second statement.

  • A bound reached through an alias. NonEmpty() is a minimum length of one and Positive() a minimum of one, so Any.String().NonEmpty().WithMinLength(8) does reach the same bound twice — and stays silent. Choosing the alias says something about intent the explicit bound does not, and which of two correct spellings to prefer is ADR-0077 opens in a new tab’s question, not this rule’s.
  • Two different bounds. WithMinLength(8).WithMaxLength(20) is a range, not a duplicate. JD031 has something to say about it; this rule does not.
  • A range form. Between(1, 50).Between(2, 20) carries two bounds per call, so no whole call dies. Out of scope here, and still covered by JD024 when it narrows nothing.
  • An exact size. WithLength and WithCount are declared once per generator and a second declaration throws, so the run time already reports them, loudly.
  • A chain asserted to throw, as the other constraint rules do.

Read the source, or correct it there opens in a new tab· Mirrored from lib-v1.0.0-preview.6