Uh oh!
There was an error while loading. Please reload this page.
fix(compilers/openapi): preserve keywords no lowering reads - #348
Merged
Conversation
This was referenced Aug 9, 2026
schemaConstraints gained a residue destination on main (#346) while this branch was hoisting the `owns` lookup above it; the merged call keeps both. main also moved the schema and operation test helpers into openapitest, which this branch's new cases still called unqualified — a conflict no textual merge sees, since either side compiles on its own. The four test conflicts are each two independent functions appended at one spot, sharing the closer that followed them, so taking both needed the first one's braces put back rather than the markers simply dropped.
minItems, maxItems and uniqueItems reached the IR in no form at all wherever the position did not lower to a List: no field, no Unmodeled entry, no diagnostic. Two documents differing only in them compiled to the same types and services, which is the loss this census exists to stop — it was simply left outside it. ir.List.Constraints is their only home. listConstraints is their only reader and only the array lowering calls it, so an object drops them; so does the Tuple that prefixItems hoists, which has no Constraints field at all. They are excluded from valueConstraintKeywords for that same reason, which is what left them belonging to no reader rather than to declaredConstraints. The census comment claimed the rest of the vocabulary was captured wherever it was written. That was the claim these three falsified, so it now says what holds and names them as the case that got past it. Three rows on the elected-family table cover the object, the tuple and the scalar that hoists an alias to carry the entry, and the array is the control that keeps nothing — it stays green when the three keywords are taken back out of the census, which is what makes it a control rather than a fourth instance. The corpus carries the same three so the oracles reach them.
Two slips from admitting the collection bounds. The paragraph explaining why the division is a claim rather than a proof repeated, verbatim, the sentence the paragraph below it already carried about a keyword in neither list. And "valueConstraintKeywords excludes them for that reason" pointed at the sentence about ir.Tuple, when the reason is that the bounds are List-owned. Comments only; no statement changed.
Uh oh!
There was an error while loading. Please reload this page.
OmarAlJarrah added a commit
that referenced
this pull request
Aug 10, 2026
Three production conflicts. load.go took both sides' Options fields and kept the byte budget where it was, before the parse, dropping this branch's copy of the pre-parse cycle scan that main moved onto the decoded tree. compose.go and architecture.md each had one side add a paragraph while the other rewrote the one beside it. The rest were the test helpers main moved into openapitest, which also gained a lowerer constructor this branch's fixtures had to adopt while keeping the Limits argument they pass. The merge also put the enum budget and the unhomed-keyword census (#348) in each other's way, which neither could see alone. A member set past the budget degrades to the top type, and ir.Any has a field for nothing, so the census preserved the whole set verbatim: 5,000 members refused at a budget of 10 came back as 50,001 bytes under Unmodeled, more than the source they were read from. The budget bounded the per-member amplification and nothing else. keywordHome now counts Any a home for `enum`, exactly as it already did for `const` and for the same reason — lowerEnum read the members and announced the degradation, so claiming them again both double-reports and undoes the refusal. typeShapedBy counts it for `type` too: the top type contradicts no declared shape, and a position only arrives there by a degradation that has already been reported. The budget test now also pins that the refused node keeps nothing, which is the half a diagnostic count does not state.
OmarAlJarrah added a commit
that referenced
this pull request
Aug 10, 2026
lowerEnum grew a guard on each side: the empty member list here, the member budget on main (#349). They cannot both hold — a count of zero exceeds no positive budget — so both are kept, the degenerate list read first, beside the members it lacks. The conformance table gained a third column on main (#342), naming the matrix rows a case witnesses. empty-enum names none: its subject is the degenerate list rather than any enum capability, and that table's own comment warns that reaching for a construct on the way to a different subject is not witnessing it. The empty-enum golden moved, because the unhomed-keyword census (#348) reaches this branch for the first time here. `type: object` beside an Enum now lands under Unmodeled with the property set that was already kept — an Enum carries neither — so the case preserves strictly more than it did, and its diagnostic names both keywords.
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 freeto 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
A schema keyword that the lowering elected for a position never reads was
dropped: no IR field, no
Unmodeledentry, no diagnostic. Two shapes of thesame bug:
$ref.format,enum,const,requiredandadditionalPropertieswritten next to a$refvanished at every position —component, property, parameter, header, allOf branch. In JSON Schema 2020-12,
and so in OpenAPI 3.1,
$refis an ordinary keyword and its siblings areconjoined with it, so this is real data.
type: stringnext to anallOfthatcomposed a Model,
formatand a bound next to aconstthat hoisted aLiteral, and so on.
Both had one root cause. The "what did the winner not consume?" census
(
preserveUnhomedKeywords) had exactly one call site, insidelower()on thebody path, so the
$refpath ran no census at all; and the census that did runwalked a hand-maintained list of six shape applicators plus a special case for
format, so anything outside that list was dropped whatever it was writtenbeside.
The census now asks the node the position actually lowered to whether it has a
field for each keyword — a Model has no type token, a Literal has no encoding
and no
Constraints, an alias over a$reftarget has none of them — insteadof consulting a list of keywords worth keeping. It runs at every position a
schema is lowered at, including the three
$refsites (refSiteRef, an allOfbranch, and the
annotation.HomeCarrierpositions, which keep the keyword onthe carrier rather than hoisting a node). Anything homeless is kept verbatim
under
UnmodeledwithReasonDegradedLoweringand one info diagnostic namingwhat the position lowered to.
Deliberately unchanged:
allOfbesidetype: objectrecords nothing, because the composed Model doesassert
object. That is the case a keyword list cannot decide and the nodecan, and it is asserted as a control.
requiredis left out of the branch census:applyCompositionRequiredreads it, so recording it would report one keywordtwice.
valueConstraintKeywordsis now the single list behind bothdeclaresValueConstraintsand the recorder, replacing a second hand-writtencopy that read model fields where the predicate read raw nodes.
A bound written beside a co-declared
oneOfis still dropped; that is adifferent mechanism (the alias is interned with nil constraints) and is filed as
#347.
Test plan
TestRefSiteKeywords_KeptAtEveryPositioncovers all five$refsiblings atthe component and property positions;
TestUnhomedKeywords_ElectedLoweringKeepsWhatItCannotReadcovers the elected-family cases and asserts the kept set whole, so keeping too much fails as
loudly as keeping too little. Controls assert zero entries and zero
diagnostics where nothing is lost.
TestParams_RefSiteKeywordsAreKeptOnTheParameter,TestHeaders_RefSiteKeywordsAreKeptOnTheHeader,TestRefSiteKeywords_AllOfBranchKeepsWhatTheAliasCannotHold.TestKeywordHome_EveryCensusKeywordHasANodeThatCarriesItguards the oneagreement the census still has to keep by hand — a keyword listed with no arm
answers "homeless" everywhere, which is exactly what a
$refsite expects, sono ref-site test could see it. Verified by planting an armless keyword.
testdata/conformance/openapi/unhomed-keywords.yamlgainsthe
$ref-site and elected-family cases, with the aliases declared abovetheir targets so the two-order oracle exercises both interning orders.
Verified that deleting a case from the fixture reddens the suite.
tests kept.
gofmt,go vet,golangci-lint,go build, and thecoverage gate at exactly 100%.
Closes#268
Closes#283