feat(justdummies): add JD014-JD015 and the constraint-analysis helpers - #371
Merged
Merged
Conversation
Opens the JustDummies.Constraints category, which front-loads to build
time the subset of the library's run-time constraint checks that is
decidable from compile-time constants. The run-time checks stay: they
cover every argument these cannot see.
Three helpers carry it. ConstantFacts reads statically-known values and
is deliberately wider than IOperation.ConstantValue, because a TimeSpan
is never a C# constant yet TimeSpan.Zero and TimeSpan.FromSeconds(1) are
as statically known as any literal — and the granularity constraints that
take one are exactly the ones worth checking. AnyChainFacts walks a
fluent chain back to its factory and yields the constraint calls in
order, syntactically and without dataflow: a rule claiming a chain is
unsatisfiable must see every constraint it carries, so a chain split
across statements is not followed rather than half-understood.
JD014 reports a constant argument the generator's own guard refuses, as
one rule over one table mirroring SizeGuard and the per-generator guards.
The library validates these in one place with one message shape, and a
reader who learns "a constant the guard rejects" has learned all of them.
JD015 reports an AnyString chain whose constant constraints admit no
value. This is the case ADR-0035 names by hand as the one an analyzer
should carry and the type system cannot.
Dogfooding corrected JD015 twice, in opposite directions, and both
corrections are now pinned by tests citing the sites that produced them.
Casing is not a character set: LowerCase/UpperCase constrain the case of
a fragment's LETTERS and say nothing about its other characters, so
UpperCase().StartingWith("ORD-") is legal — the first version modelled
casing as a pool and flagged a chain AnyStringTests asserts is legal. And
the length budget, which the library does compute, no longer applies once
OneOf is declared: the fragments are then matched against the pooled
values rather than laid out side by side, which is why
OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba") is legal.
Both were verified against the built library before being encoded, not
inferred from reading the guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Opens the
JustDummies.Constraintscategory and lands the shared helpers the remaining constraint rules need. These rules front-load, to build time, the subset of the library's run-time constraint checks that is decidable from compile-time constants — the run-time checks stay, since they cover every argument these cannot see.Type of change
Changes
ConstantFacts— reads statically-known values, deliberately wider thanIOperation.ConstantValue: aTimeSpanis never a C# constant, yetTimeSpan.ZeroandTimeSpan.FromSeconds(1)are as statically known as any literal, and the granularity constraints that take one are exactly the ones worth checking.AnyChainFacts— walks a fluent chain back to its factory and yields the constraint calls in order. Syntactic, without dataflow: a rule claiming a chain is unsatisfiable must see every constraint it carries, so a chain split across statements is not followed rather than half-understood.RejectedConstantArgument(Warning) — a constant argument the generator's own guard refuses, so the call throws every time it runs. One rule over one table mirroringSizeGuardand the per-generator guards:Between(10, 5),MultipleOf(0),WithLength(-1),WithScale(29),WithChars(""),OneOf(),WithGranularity(TimeSpan.Zero), and the rest.StringConstraintsAdmitNoValue(Warning) — anAnyStringchain whose constant constraints admit no value. This is the case ADR-0035 names by hand as the one an analyzer should carry and the type system cannot:Numeric().StartingWith("ORD-")conflicts whileNumeric().StartingWith("123")does not, from identical call sites and identical static types.Dogfooding corrected JD015 twice, in opposite directions
Both corrections are now pinned by tests that cite the sites which produced them.
1. Casing is not a character set. The first version modelled
UpperCase()as the poolA-Z0-9, and flaggedAny.String().UpperCase().StartingWith("ORD-")— a chainJustDummies.UnitTests/AnyStringTests.csasserts is legal, with the display name "fragments keep their own characters". ReadingStringSpec.ValidateFragmentCharactersconfirms it: casing checksFirstAgainstCasing, which looks only at letters. So the-passes, whileUpperCase().StartingWith("abc")correctly throws on the lowercasea.2. The length budget is real, but a value set switches it off. I had also suspected the budget was unsound (overlapping
Containingfragments can share characters). A probe against the built library says otherwise —WithMaxLength(6).StartingWith("SKU-").EndingWith("-EUR")does throw. What actually explains the second hit isOneOf: with a terminal value set declared, the fragments are matched against the pooled values rather than laid out side by side, soOneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba")is legal even though the fragments sum to 4.Both facts were verified against the built library before being encoded, not inferred from reading the guards. After the corrections, JD014 and JD015 produce zero hits across
JustDummies.UnitTests,JustDummies.PropertyTests,JustDummies.Xunit.UnitTests,FirstClassErrors.Testing.UnitTestsandFirstClassErrors.UnitTests.Testing
dotnet build FirstClassErrors.sln— 0 warnings, 0 errorsdotnet test FirstClassErrors.sln— 2 042 tests, 0 failures across all 13 test projectsFirstClassErrors.Analyzers.UnitTests, 132) — the rules added here live inJustDummies.Analyzers.UnitTests(125)Mutation testing was not run locally.
Documentation
doc/updated —JD014/JD015pages and a new Constraints section in the rule indexJD014.fr.md,JD015.fr.mdand the French indexArchitecture decisions
Proposed: ADR-____Worth a second opinion, though: this category's premise — that front-loading the constant subset of the run-time checks is desirable, and that the run-time checks stay — is arguably a lasting decision. My study drafted it as one. I have not written the ADR, because ADR-0035 already states the rule these follow ("an analyzer is the right tool for a value-dependent smell the types cannot catch") and JD015 is the very example it names, so this looks like application rather than decision. Tell me if you read it the other way and I will draft the successor.
Remaining
This is the first of the constraint waves. Fifteen catalogued rules remain: the scalar-chain emptiness test, collection counts and cardinality, the enum universe, no-effect constraints, duplicate pool values, the empty relative URI, the unused
Combineoperand,Distinctover reference equality, and the reproducibility long tail. Two carry their own decision and I will surface them rather than decide alone — the unsupported-regex rule needs the grammar shared with the library (a linked source file), and the production-code rule needs theIsTestProjectMSBuild oracle, withFirstClassErrors.Testingas a live counterexample.🤖 Generated with Claude Code
https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq
Generated by Claude Code