Repository navigation
fix(compilers/openapi)!: type a collection bound beside a $ref - #1302
Merged
Merged
Conversation
A collection bound written for one use of a shared list stayed verbatim in Unmodeled, though ir.Constraints holds it on a carrier and on an alias. ConstraintsOf leaves the three keywords to the collection nodes, and a position that lowers to a reference has none. minItems, maxItems and a true uniqueItems beside a $ref, a collapsed union or an expanded $dynamicRef now reach the Constraints of the property, parameter, header or alias holding that use, as maxLength beside the same $ref does. The read stays out of ConstraintsOf, which also serves nodes: there it would restate every list property's bound on its carrier and write one onto a Model or Scalar that the census keeps verbatim. A position owning its node keeps its bound there. The census counts a bound as homed only where the reader fills it, so two stay verbatim as they were: a false uniqueItems, which a plain bool cannot tell from unset and which would stop an alias being a decorator, and a count past int64, which the parser reads as 0. Breaking: IRVersion moves to 0.24.0. No key changes, but a consumer pinned to 0.23.0 that reads openapi:minItems, openapi:maxItems or openapi:uniqueItems at these positions no longer finds them, and an alias holding such a bound is no longer a decorator.
A bound beside a reference is read from the parser's model and held to its raw node, so a test on plain literals says nothing of the spellings the two read apart. Pin a YAML alias, a merge key, hex, octal and separated integers, and both sides of the int64 boundary, one of which a truncating read would hold as a false bound. Pin too that a parameter and a header reached through a $ref leave a bound to the List they lowered to, where a carrier's pointer could differ from the schema's. CountIsExact now asks the model first, so a position writing no bound does no node lookup.
Two collection-bound tests named a local after the package they import, and one table branched on its row. Rename the local and hold the fold's conflicts to a count, so the table stays flat.
This was referenced Nov 1, 2026
The constraints rule said a collection bound beside a reference that Constraints cannot state is kept verbatim. That holds at a $ref site and a collapsed union, which run the keyword census, but an expanded $dynamicRef runs none (#848), so a false uniqueItems or a count past int64 beside one is dropped there. The 0.24.0 history entry likewise said every such position had kept its bound verbatim, where an expanded $dynamicRef had dropped it. Scope the sentence to the positions it is true of, name the drop in the rule's Status, and name #1312 beside #1269 for a bound on an inline allOf branch, which needs that branch to fold as well.
13 tasks done
main took IRVersion 0.24.0 for #1219, so this change moves to 0.25.0, with its own generations row and history entry. #1217 made an expanded $dynamicRef run the keyword census. That census asks keywordHome at no node, which now homes a collection bound the reader fills, so the keywords-at-every-outcome carrier holds its maxItems in Constraints. The case now writes a false uniqueItems beside it too, which stays verbatim, so the census at that carrier is still pinned. The rule's Status drops the items #1217 closed and the clause saying a bound beside an expanded $dynamicRef is dropped.
With main's census at an expanded $dynamicRef, a collection bound the reader cannot state there, a count past int64 or a false uniqueItems, is kept verbatim and reported, as beside a $ref, where it had been dropped. The test said it was dropped and asserted only that no Constraints was written, so it passed whether the census ran or not. It now asserts the kept keywords and their report, and the rule's sentence on what stays verbatim names every position lowering to a reference.
#1226 took IRVersion 0.25.0 for a shape change, so this change moves to 0.26.0 on that shape, with its own generations row and history entry. The new case's golden gains the unresolved auth namespace every document now declares.
#1307 made the allOf fold intersect a redeclared field's bounds, the collection bounds among them. A bound this change puts on a carrier therefore folds the same way: two declarations writing minItems 1 and 2 give 2 in either order, with nothing kept and nothing reported, where the fold had kept the first and reported the other. The test pinning the fold follows.
An element alias over a $ref now holds the bound written beside it, so the allOf fold judges two redeclared lists alike in all but that bound by it: the narrower wins in either order, and two whose bounds meet overlap, keeping the first and reporting the other. Before, each alias kept its bound verbatim and the two lists were reported as different types.
TestParams_ASharedDeclarationLeavesItsListItsBound checked that neither carrier restated the bound and that each type was a List, but not that the List held the bound, so it passed with every List's bounds dropped. It now asserts the bound on each List.
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 2, 2026
#1302 took 0.26.0 for a change of meaning, with the shape of 0.25.0, so this change moves to 0.27.0 and keeps its shape hash. Its history entry now names 0.26.0 as the version an older consumer is pinned to.
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 2, 2026
Brings in #1302 and #1304. - #1302 took IRVersion 0.26.0, so this branch's bump moves to 0.27.0, its row keeping 0.26.0's shape. - #1302 reads a collection bound into what a position lowered to a reference owns, through CarrierConstraintsOf and referenceConstraintsOf. A refinement lowers to a reference, so each takes the voices ConstraintsOf takes here, and a bound beside an allOf refining a $ref is typed on the alias or the carrier, which TestCollectionBound_BesideARefinementReachesWhatThePositionOwns pins. A bound on its branch still stays with the branch (#1312), so referenceConstraintsOf's comment and the constraints rule's status, which said such an allOf lowers to a Model, say that instead.
17 tasks done
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
minItems,maxItemsanduniqueItemsreachedir.Constraintsonly on the node an array schema lowers to. Written for one use of a shared list, they stayed verbatim inUnmodeled, although the property, parameter, header or alias holding that use has the field andmaxLengthbeside the same$refalready reached it. A consumer validating a field read the typed field and found no bound.mainHolder.besideconstraints: null,unmodeled: {"openapi:minItems": 1}constraints: {minItems: 1}Element/itemsconstraints: null,unmodeled: {"openapi:maxItems": 3}constraints: {maxItems: 3}openapi/degraded-constructat eachA position that lowers to a reference has no collection node of its own, so what it owns now holds the bound: the alias, or the property, parameter or header carrying it. That covers a
$refsite, a collapsed union and an expanded$dynamicRef. The rule isdocs/compilers/openapi.md#constraints.Closes #1287
Decisions a reviewer should weigh
ConstraintsOf, but that reader also serves nodes. Tried as a mutant, it restates a bound on the carrier of every list property and writes one onto aModelorScalarthat the census also keeps verbatim. SoConstraintsOfis unchanged, andreferenceConstraintsOfreads them only where the census asks about a reference: the aliashoistDeclarationHomeinterns, and a carrier whose position owns no node, the linePreserveRefSiteKeywordsalready draws. A position owning aListorTuplekeeps its bound there.referenceHolds). Two stay verbatim, as onmain, with their report.uniqueItems: falsehas no spelling a plain bool can tell from unset, and a non-nilConstraintsfor it would stop an alias being a decorator. A count pastint64reads as0in the parser's model (openapi: an integer bound past 64 bits lowers as 0 #1050), so homing it would replace a faithful record with a false bound;annotation.CountIsExactholds the model to the raw literal, and1.0and1e3still reach the field.{$ref: Str, minItems: 1}holds the bound as{$ref: Obj, maxLength: 1}holds its own:Constraintssays what the position wrote, and reading the target would let declaration order in.$refsite.#union-null-variant's text listed a collection bound among what no reference holds, a list inherited from the$refrule it says the collapse follows, and openapi: a oneOf of one schema and null drops its discriminator when it collapses to a nullable reference #595 was about discriminators, so nothing there settled the bound. The test pinning it changed with the rule.What else the bound now reaches
Reading each consumer of
Constraintsfor what it did with these positions:merge's fold: a redeclared field's bound joins the intersection the fold takes of a value bound (fix(compilers/openapi): intersect a redeclared field's bounds #1307), the largerminItemsand the smallermaxItemsin either order, with nothing kept or reported, wheremainkeeps the first declaration's verbatim and reports the other. Two redeclared lists whose element aliases differ only in such a bound are judged by it: the narrower wins in either order, and two that meet keep the first and report the other, wheremainreports them as different types.merge's admission: an alias's bound is judged.maintook adefault: [[a, b]]over elements bounded bymaxItems: 1; it is now refused, andmaxItems: 2still takes it.ir.Decorated: an alias holding a bound is no decorator, which is whatdocs/emitters/refine/decorators.mdsays of a constraint. An alias over a list is then the casedocs/emitters/refine/scalars.mdcovers: "its constraints are checked where it is used".pass,irverifyandirfactsread noConstraints:grep -rn '\.Constraints\b' ir pass --include='*.go' --exclude='*_test.go'finds onlyir.Decoratedand theModel-to-MapTform.Statements read for the old values, by
git grep -nE 'MinItems|MaxItems|UniqueItems|minItems|maxItems|uniqueItems' -- docs: the two that assumed them are changed,#constraintsand#union-null-variant, anddocs/ir/annotations.mdgains the carrier's half. TheTupleand emitter sentences read a tuple's ownMinItems, and still hold.Breaking
IRVersionmoves to 0.26.0. No key changes shape, so thegenerationsrow records the meaning. A consumer pinned to 0.25.0 that readsopenapi:minItems,openapi:maxItemsoropenapi:uniqueItemsat these positions no longer finds them, and finds an alias holding such a bound is no longer a decorator.Limits
Each is stated at the code or rule that has it.
allOfspelling is untouched here.{allOf: [{$ref: Tags}], minItems: 1}lowers to aModel, which keeps the bound verbatim, until fix(compilers/openapi)!: lower allOf by what its conjuncts say #1269 makes it a use of the reference (referenceConstraintsOf's comment, and the rule's Status). A bound on an inline branch,{allOf: [{$ref: Tags}, {minItems: 1}]}, also needs that branch to fold, which neither PR does: openapi: a collection bound on an inline allOf branch beside a $ref is kept verbatim #1312.#shape-applicator). The golden spec has 112 of these, 102 of themuniqueItemson an object:jq '[.. | objects | select((has("minItems") or has("maxItems") or has("uniqueItems")) and has("type") and .type != "array")] | length' openapi.json.docs/ir/annotations.md#what-a-reference-carries-downleaves on the target's node.Golden spec
This changes nothing on
cloudflare/api-schemas@319dc756by itself: the direct construct does not occur there, and all 9 uses the issue counts are theallOfspelling.At
700002e0againstmain@7cf3f95f, the IR and diagnostics are identical past the version stamp, and both exit 1, asmaindoes on this spec.A trial merge of this branch's first head,
7c089590, with #1269 @2af95505measured those 9 uses:allOfreach the carrier with no further change, since fix(compilers/openapi)!: lower allOf by what its conjuncts say #1269's refinement asks the census about a reference.foldEntrytakescollectionBoundKeywords, andreferenceConstraintsOfreads the voices. openapi: a collection bound on an inline allOf branch beside a $ref is kept verbatim #1312 tracks it, with the reproduction.anyOfbranches that neither change lowers.Other open PRs
params.go,hoist.goandproperty.go, where itsvoicesparameter meets these readers; its other conflicts are withmain. Whichever lands second threads it throughCarrierConstraintsOfandreferenceConstraintsOfand carries the fold above.IRVersion, and whichever of the two lands second takes the next one.Test plan
ref-collection-bounds, covering property, parameter, header, media-type schema,items, component alias and collapsed union, withplainas the control. The order-invariance oracle reaches it through the corpus.schema.TestCollectionBound_*for each position, an expanded$dynamicRefamong them, with what its census keeps verbatim, each spelling of a count, both sides of theint64boundary, what stays verbatim, one home per declaration, the other shapes, 3.0, the fold, admission, and a$refnaming the position in both declaration orders;operation.TestParams_CollectionBoundBesideARefReachesTheCarrierandTestParams_ASharedDeclarationLeavesItsListItsBound;annotation.TestCountIsExact.keywords-at-every-outcome, from fix(compilers/openapi): keep a keyword at every schema outcome #1217: themaxItemsits carrier writes beside an expanded$dynamicRefnow reachesConstraints, so the carrier also writes a falseuniqueItems, which stays verbatim and keeps that carrier's census pinned.TestCollectionBound_ARedeclaredFieldFoldsItpins the intersection in both orders andTestCollectionBound_RedeclaredListsAreJudgedByTheirElementsBoundsthe element case. The first reddens withminItemsfolded to the smaller count, and the second with an element alias holding no bound.TestParams_ASharedDeclarationLeavesItsListItsBoundnow asserts eachListholds its bound; it had passed with everyList's bounds dropped.7c089590in a throwaway worktree, each killed by a named test: each read skipped or widened, the census homing what the reader left and homing nothing, a falseuniqueItemsread as held, any count trusted, no alias hoisted, the parameter skipping the carrier read, the issue'sConstraintsOfread, and a truncating read of a count. One first survived, which showed an assertion inANodeOfAnotherShapeKeepsItVerbatimcould never match; it now reads the field.main, planted 13 defects in the readers and the census at9cb0747c, each killed by a named test, the census at an expanded$dynamicRefswitched off among them, which the test's earlier assertions let through.sweep.pyovertestdataand a corpus of real specs, at700002e0againstmain@7cf3f95f: onlyref-collection-boundsandkeywords-at-every-outcomediffer past the version stamp, and the exit codes match (1,659 at 0, 520 at 1).main@39afce9band9cb0747c: 24 positions (component, property, nested and union properties,items,prefixItems, both catch-alls, union andallOfbranches,not,then,$defs,dependentSchemas,contentSchema, parameters and headers inline and shared, both bodies, and positions an outside$refnames), 8 spellings (a$ref, one to a nullable list, aoneOfand ananyOfcollapsing to one, an expanded$dynamicRef, anallOfover a$ref, and an inline and a nullable list as controls), and 5 markers (minItems,maxItems, a true and a falseuniqueItems, and1e400). Exit codes and errors match, and no marker changes its count of homes. At every reference spelling a valid marker moves from verbatim toConstraintswherevermainkept it as an entry of its own; theallOfspelling and both controls are unchanged; no falseuniqueItemsor1e400beside a reference is typed. Undernot,then,dependentSchemasand an inlineallOfbranch the marker is held raw in that keyword's verbatim entry, alike on both.7c089590, diffed two declaration orders of one document holding every carrier, shared parameters and headers, and an outside$refnaming each position: no difference in the document or the diagnostics.9cb0747c.