Refuse Void anywhere but a return type or a Result's value [minor] - #198
Merged
Merged
Conversation
Void was refused as a parameter and nowhere else. A wrapper is transparent to
everything else in validation - a class named inside an Optional is checked
exactly as one named directly is - and that transparency is what let it through:
a member typed Void, or an Optional<Void> or Span<Void> anywhere, validated
cleanly and then emitted `void x{};`, `std::optional<void>` or `std::span<void>`,
none of which are types. The compiler that refused them was the consumer's,
pointing at generated code rather than at the schema that produced it. The
editor's type picker offers Void for return types and the same picker serves
members, so it is a reachable edit rather than a contrived one.
ValidateType now carries a TypePosition down the descent, and ValidateTypeStandsHere
asks the question once on the way down rather than at each call site. The two
positions an absent value may stand in are a function's return type and the value
a Result carries, which is how "can fail, produces nothing" is spelled and is a
type in both target languages.
None is reported on the same descent, but only below a declaration: a member, a
parameter and a return type each already say what is unfinished in their own
words, and a vector says what its components have to be, so those keep their one
message. Below them nobody was saying anything, so an Optional<None> reached the
generator and threw there.
AbsentTypeValidationTests covers both halves - the shapes now refused, and the
six places that must not gain a second message. CppTypeMappingTests pins why
validation has to be the gate for Void in particular: unlike None, the mapper has
an answer for it wherever it is asked, so generation cannot be what catches it.
Fixes #172
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhsxzNdL3VieTYFXFpwmov
github-code-quality flagged the loop for mapping its iteration variable straight to another one. A row per case is what the wrapper cases beside it already do, and it says which of the two failed rather than stopping at the first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JhsxzNdL3VieTYFXFpwmov
|
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.



Fixes #172
What was wrong
ValidateParameterrefused a top-levelVoidparameter, and nothing else refusedVoidat all. A wrapper is transparent to everything else in validation — a class named inside anOptionalis checked exactly as one named directly is — and that transparency is what let it through. Member-level checks tested only forNone.So all four of these validated cleanly:
Voidvoid value{};Optional<Void>std::optional<void>Span<Void>std::span<void>Optional<None>CppGenerationExceptionat generation timeNone of those three C++ spellings is a type. The compiler that refused them was the consumer's, pointing at generated code rather than at the schema that produced it. The editor's type picker offers
Voidfor return types and the same picker serves members, so this is a reachable editing state rather than a contrived one.CLAUDE.mdstates the invariant as "noArray,ResultorVoidparameters, noNoneanywhere generatable". Neither half held below the top level.The fix
One shared choke point, as the issue's analysis and triage both proposed.
ValidateTypenow carries aTypePositiondown the descent, and a newValidateTypeStandsHereasks the question once on the way down rather than at each call site.TypePositionnames the only two positions an absent value may stand in:Return— a function's return type, which is whatVoidis for.ResultValue— the value aResultcarries.Result<Void>is the honest spelling of "can fail, produces nothing" and it is a type in both target languages:std::expected<void, E>and the one-argumentResult<TError>. The exemption is theResult's own value and does not carry through a wrapper inside it, soResult<Optional<Void>>is still refused.The other two positions —
Declared(a member's or parameter's type as written) andNested(inside a wrapper, an array or a vector) — refuseVoid.Noneis reported on the same descent, but only below a declaration. A member, a parameter and a return type each already say what is unfinished in their own words, and a vector says what its components have to be, so each keeps its one message. Below them nobody was saying anything, which is whyOptional<None>reached the generator and threw there.Two carve-outs keep one mistake to one message:
NoneorVoidstill gets the message naming what a component has to be, which is more use than being told thatVoidcarries no value.The existing
Voidparameter message ("Remove it") is unchanged — it names what to do, which the general message cannot.Tests
Schema.Test/AbsentTypeValidationTests.cs(19 cases) covers both halves: the shapes now refused, and the six places that must not gain a second message.Verified the tests catch the bug rather than merely passing. Restoring the pre-fix
Schema.Validation.csand re-running fails exactly the 13 bug-catching cases, while the 6 over-reporting guards pass either way — which is what they are for:CppTypeMappingTests.VoidIsRefusedByTheSchemaRatherThanByTheMappercovers the third acceptance criterion — that validation is the actual gate, not generation-time luck. It asserts both halves together: the mapper does emit the three ill-formed spellings, and the schema refuses the same three before a generator is reached. That pairing matters forVoidspecifically, because unlikeNonethe mapper has an answer for it wherever it is asked, so generation cannot be what catches it. This test also fails on the reverted fix.Verification
dotnet build Schema.sln -c Release— 0 warnings, 0 errorsSchema.Test— 497/497 passing (478 before this branch)Schema.Cpp.Test— 103/103 passingSchema.Editor.Test— 17/17 passingThe
samples/breadth suite still validates clean: neither sample has aVoidoutside a return type, whichLegacySampleGenerationTestsalready relied on.Docs
The
CLAUDE.mdparagraph stating the invariant now says where it is enforced and names the two exempt positions.CHANGELOG.md/VERSION.md/AUTHORS.mdare pipeline-generated and untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01JhsxzNdL3VieTYFXFpwmov
Generated by Claude Code