Repository navigation
fix(compilers/openapi): warn on a 3.0 array without items - #724
Conversation
An OpenAPI 3.0 array schema that writes no `items` now reports
`openapi/missing-array-items` at the schema's own pointer, and its element
type stays the top type exactly as before. The 2020-12 dialects (3.1 and 3.2)
are silent, and so is a 3.0 schema that wrote the keyword — including
`items: {}`. The guard keys on the keyword's absence, never on the element
having lowered to `any`.
A 3.0 document that writes `prefixItems` reaches the tuple lowering, where
`items` is never consulted; what that reveals is a separate defect, left
untouched here.
OmarAlJarrah
left a comment
There was a problem hiding this comment.
Review of the merged result (this branch with main merged in), probed with 3.0 specs rather than read.
Must fix: the two doc comments over the 100-word cap (inline). go test ./internal/archtest is the only failing package on the merged tree; the rest of go test ./... and golangci-lint on the touched packages pass.
Inline, in order of weight: the unfiled prefixItems defect, the "at most once per pointer" claim that does not hold on main, the once-ness tests that cannot see a duplicate, the entry-document dialect limit, Ctx method budget and duplicated tests, expensive-first condition order, and a garbled test comment.
Not attached to a diff line:
- No corpus spec reaches the new code. I validated all 241 files under
testdata/with a build frommainand one from this branch merged withmain: identical output for every file, nomissing-array-itemsanywhere. Only two documents there are OpenAPI 3.0 (streaming-media-30,nullable-30) and neither contains an array. The testing section of the repo's contributor guide says the usual way to cover a new construct is a spec the order-invariance sweep reaches; the siblingsempty-enum,path-item-anchored-keyandquerystring-forbidden-keywordseach have a conformance case with a golden. A smalltestdata/conformance/openapi3.0 case with the$refdeclared before the array's owner would put the new code under the sweep and the golden diff. - Description claim with no test. The description lists "a variant pointer under
type: [...]" as a possible location. It works ({type: [array, string]}in 3.0.3 reports at/components/schemas/T/type/0), but no row pins it. Add a row or drop the mention. - Description wording. The "
compile.Types.Internruns the build closure at most once per pointer" sentence needs the same correction as the code comment (see the inline note onaccumulate.go), and the "separate defect, left as it is here" sentence needs the issue number once filed.
Checked and fine: every carrier I tried warns once at its own pointer (path, query, header and cookie parameters; request and response bodies including default; headers; form and multipart properties; callbacks; shared components referenced from several operations, in both path orders); items: {}, items: true, nullable, $ref siblings and the 3.1/3.2 dialects stay silent; the oracle sweep passes on every valid probe. Unreferenced components/parameters, headers, requestBodies and responses are never lowered at all, for any diagnostic, so their silence here is existing reach, not part of this change.
| // MissingArrayItems reports an OpenAPI 3.0 array schema that writes no | ||
| // `items`. 3.0 states the array form through the keyword alone, so such a | ||
| // schema declares an array without saying what its elements are — a | ||
| // document defect in that dialect (GitHub #650). The schema still lowers, | ||
| // as a List whose element type is the top type, exactly as it would | ||
| // without a diagnostic. | ||
| // | ||
| // Warning, for the reason its neighbours ReservedHeaderName and | ||
| // UnknownObjectKey are: it is a document defect that still lowers, and | ||
| // harness.Check stops at the first error diagnostic, which would hide | ||
| // every later finding in any spec containing such an array. Not an error | ||
| // for the second reason too — nothing the document declared is dropped, | ||
| // since the element type the IR carries is the one it carries today, so | ||
| // there is no losslessness failure to escalate (invariant 2 is not in | ||
| // question). | ||
| // | ||
| // A code of its own rather than DegradedConstruct, because codes are the | ||
| // unit a consumer allowlists (ir-design §13). DegradedConstruct names a | ||
| // construct kept in a weaker shape; here the document wrote no shape to | ||
| // weaken, and the lowering is lossless — it is the *document* that is | ||
| // incomplete, which is the class EmptyEnum and UnknownObjectKey name. |
There was a problem hiding this comment.
This doc comment is 192 words; the cap is 100 (TestDocComments_StayUnderTheCap, added in #745 after this branch's base). The gate was green when this was pushed, but with main merged in go test ./internal/archtest fails:
diag.go:334: MissingArrayItems: doc comment is 192 words; the cap is 100
The severity reasoning is already in the PR description, which is where the excess belongs. A version that fits (checked against the archtest on the merged tree):
| // MissingArrayItems reports an OpenAPI 3.0 array schema that writes no | |
| // `items`. 3.0 states the array form through the keyword alone, so such a | |
| // schema declares an array without saying what its elements are — a | |
| // document defect in that dialect (GitHub #650). The schema still lowers, | |
| // as a List whose element type is the top type, exactly as it would | |
| // without a diagnostic. | |
| // | |
| // Warning, for the reason its neighbours ReservedHeaderName and | |
| // UnknownObjectKey are: it is a document defect that still lowers, and | |
| // harness.Check stops at the first error diagnostic, which would hide | |
| // every later finding in any spec containing such an array. Not an error | |
| // for the second reason too — nothing the document declared is dropped, | |
| // since the element type the IR carries is the one it carries today, so | |
| // there is no losslessness failure to escalate (invariant 2 is not in | |
| // question). | |
| // | |
| // A code of its own rather than DegradedConstruct, because codes are the | |
| // unit a consumer allowlists (ir-design §13). DegradedConstruct names a | |
| // construct kept in a weaker shape; here the document wrote no shape to | |
| // weaken, and the lowering is lossless — it is the *document* that is | |
| // incomplete, which is the class EmptyEnum and UnknownObjectKey name. | |
| // MissingArrayItems reports an OpenAPI 3.0 array schema that writes no | |
| // `items`, so declares an array without saying what its elements are. It | |
| // still lowers, as a List over the top type. A warning rather than an | |
| // error: nothing the document declared is dropped, and harness.Check stops | |
| // at the first error diagnostic. A code of its own rather than | |
| // DegradedConstruct, because nothing is kept in a weaker shape. |
It also drops the (ir-design §13) cite. §13 defines Diagnostic and says codes are stable, but the allowlist text is in docs/architecture.md §4 and engine/diag.go. The same cite already exists at line 22, so this is inherited rather than new; it just gets copied here.
There was a problem hiding this comment.
Applied your text in 491c471, with the §13 cite dropped. go test ./internal/archtest is green on the merged tree.
| // ArrayItemsRequired reports whether this document's dialect requires an array | ||
| // schema to write `items`: true under OpenAPI 3.0, where the keyword is how the | ||
| // array form is stated at all, and false under the 2020-12 dialect of 3.1 and | ||
| // 3.2, where omitting it is legal and leaves an array with no constraint on its | ||
| // elements. An unrecognized version defaults to false, the 2020-12 answer, for | ||
| // the reason ExclusiveBoundIsBoolean beside it defaults to the numeric form. | ||
| // | ||
| // It reads the document version, which makes it a question about the context | ||
| // rather than about any schema, and it is read through its accessor so a | ||
| // context with no document answers like an unrecognized version rather than | ||
| // panicking — which is how the other readers here behave on a zero value. |
There was a problem hiding this comment.
Also over the cap: 120 words against 100. With main merged in the archtest reports lowering.go:355: Ctx.ArrayItemsRequired: doc comment is 120 words; the cap is 100. The last paragraph repeats what ExclusiveBoundIsBoolean's comment already says about the zero value. A version that fits, which also states the entry-document behaviour discussed on the next line:
| // ArrayItemsRequired reports whether this document's dialect requires an array | |
| // schema to write `items`: true under OpenAPI 3.0, where the keyword is how the | |
| // array form is stated at all, and false under the 2020-12 dialect of 3.1 and | |
| // 3.2, where omitting it is legal and leaves an array with no constraint on its | |
| // elements. An unrecognized version defaults to false, the 2020-12 answer, for | |
| // the reason ExclusiveBoundIsBoolean beside it defaults to the numeric form. | |
| // | |
| // It reads the document version, which makes it a question about the context | |
| // rather than about any schema, and it is read through its accessor so a | |
| // context with no document answers like an unrecognized version rather than | |
| // panicking — which is how the other readers here behave on a zero value. | |
| // ArrayItemsRequired reports whether this document's dialect requires an array | |
| // schema to write `items`: true under OpenAPI 3.0, false under the 2020-12 | |
| // dialect of 3.1 and 3.2. An unrecognized version, and a context with no | |
| // document, answer false, as ExclusiveBoundIsBoolean does. It reads the entry | |
| // document's version, whichever document holds the schema. |
There was a problem hiding this comment.
Applied in 491c471. The comment is now on Ctx.IsOpenAPI30, since the two accessors were merged into one (see the thread below), and it states the entry-document limit.
| // context with no document answers like an unrecognized version rather than | ||
| // panicking — which is how the other readers here behave on a zero value. | ||
| func (c Ctx) ArrayItemsRequired() bool { | ||
| minor, _ := load.SupportedMinor(c.Doc.GetOpenAPI()) |
There was a problem hiding this comment.
The dialect is read from the entry document (c.Doc), not from the document that holds the schema, although Ctx tracks foreign scope (foreign/holder). With allow-external-refs=true, a 3.0.3 root whose response is a $ref into an openapi: 3.1.0 file with an inline schema: {type: array} gets missing-array-items ("3.0 requires items") for a schema that is legal in its own document. The reverse (3.1 root, 3.0 file) stays silent.
This is the same limit ExclusiveBoundIsBoolean has, and there it is worse: a 3.1 file's numeric exclusiveMinimum reached from a 3.0 root is reported as an invalid-exclusive-bound error. So I would not block on it, but a limitation like this should be stated where the next reader will reach it, and neither the comment nor the PR description says it. The suggested doc comment above covers the code side; the description could carry one sentence.
There was a problem hiding this comment.
Agreed it shouldn't block. The limit is now stated in IsOpenAPI30's doc comment (491c471): it reads the entry document's version, so a 3.1 file reached by $ref from a 3.0 root is read as 3.0. The PR description will carry the same sentence, along with the note that ExclusiveBoundIsBoolean already had this limit.
| // rather than about any schema, and it is read through its accessor so a | ||
| // context with no document answers like an unrecognized version rather than | ||
| // panicking — which is how the other readers here behave on a zero value. | ||
| func (c Ctx) ArrayItemsRequired() bool { |
There was a problem hiding this comment.
Design note: Ctx goes from 17 to 18 methods (the cap is 20), and this accessor is the same two lines as ExclusiveBoundIsBoolean: a version re-parse through load.SupportedMinor and a == "3.0" comparison. The description names a next dialect defect (a 3.1-only keyword read from a 3.0 document), which would take another method and another copy of the explanation.
One isOpenAPI30 helper, or a single dialect accessor, that both call would keep this at one method per question about the dialect rather than one per 3.0-ism, and lets one test table serve both.
There was a problem hiding this comment.
Took the single-accessor option: ExclusiveBoundIsBoolean and ArrayItemsRequired are now one Ctx.IsOpenAPI30(), and both call sites pass its answer (491c471). Ctx is back to 17 methods, and the next 3.0-ism costs no new method.
| diags = append(diags, tupleDiags...) | ||
| return t | ||
| } | ||
| if c.ArrayItemsRequired() && s.GetItems() == nil { |
There was a problem hiding this comment.
Nit: the conjunction runs the expensive operand first. ArrayItemsRequired() calls load.SupportedMinor, which does a strings.SplitN and a concatenation, for every array schema lowered (every 3.1/3.2 array, and every 3.0 array that does write items), before the nil check on GetItems() that rules most of them out. s.GetItems() == nil && c.ArrayItemsRequired() is the same condition. I swapped them locally and the package tests stay green, as they should.
There was a problem hiding this comment.
Swapped to s.GetItems() == nil && c.IsOpenAPI30() (491c471).
| // prefixItems does not reach this branch at all — the Tuple | ||
| // lowering above never consults items — and what that reveals, a | ||
| // 3.1-only keyword read from a 3.0 document with no diagnostic, is | ||
| // a separate defect deliberately left untouched here (GitHub #650). |
There was a problem hiding this comment.
The 3.1-only-keyword-in-a-3.0-document defect is described here, in the commit message and in the description as "a separate defect, left untouched", but there is no issue for it. I searched the tracker (prefixItems, "3.1-only keyword", "dialect", open 3.0 titles): #597 is about tuple openness, not this. (GitHub #650) points at the issue this PR closes, so after merge the next reader has nowhere to follow it.
A 3.0.3 document with type: array and prefixItems: [{type: string}] compiles with no diagnostic at all today. Please file it and cite that number here and in the description (the repo convention is to record an out-of-scope defect somewhere the next reader will reach).
There was a problem hiding this comment.
Confirmed: a 3.0.3 document with type: array and prefixItems: [{type: string}] compiles to a Tuple with no diagnostic at all. I searched the tracker too and found nothing covering it (#597 and #146 are different defects). An issue is being opened, and its number will be cited in this comment and in the description.
| // internNode runs this build at most once per pointer, so a sibling | ||
| // $ref that reaches the declaration first still reports it once. |
There was a problem hiding this comment.
This claim is not true on main. internNode goes through Types.InternDeclared, which, when a reference built the node first, "builds it again under the same ID" (its own doc comment, and #750: it replaces what the reference built, not what it reported). So the guard fires twice for the same pointer when the reference comes first.
Reproduction: 3.0.3 with A.properties.copy: {$ref: '#/components/schemas/Holder/properties/tags'} declared before Holder.properties.tags: {type: array}. With a print in the guard it fires twice for /components/schemas/Holder/properties/tags in that order and once with the two swapped. The user sees one warning in both because compile.Diags drops identical diagnostics, so the observable behaviour is right, but the reason is the dedup, not once-ness of the build. The description makes the same claim ("compile.Types.Intern runs the build closure at most once per pointer"); both should say the single report comes from Diags deduplication.
There was a problem hiding this comment.
Reproduced. With a counter on the raw lowering diagnostics, the coordinate-$ref-first order reports twice at /components/schemas/Holder/properties/tags, and the declaration-first order reports once. The comment now says the declaration is rebuilt by Types.InternDeclared, and that the caller sees one warning because compile.Diags drops the identical second report (491c471). The description will be corrected the same way.
| wantWarnings: []string{"/components/schemas/Holder/properties/tags"}, | ||
| }, | ||
| { | ||
| name: "3.0 sibling $ref to the bare array warns once", |
There was a problem hiding this comment.
These "warns once" rows cannot detect a duplicate report. lowerSpec collects through compile.Diags, which drops identical diagnostics, so a guard that appended the warning twice leaves TestLower_ListWithoutItems and TestCompile_MissingArrayItemsReachesTheDiagnostics green (I tried exactly that). Moving the pointer to /items, changing the severity, dropping either conjunct all turn tests red, so only the once-ness claim is unguarded.
This row also has a single declaration order and a plain-name $ref, which never takes the rebuild path (see the comment on accumulate.go). A row where the $ref targets the coordinate #/components/schemas/Holder/properties/tags and is declared before Holder exercises it, and counting the raw lowering diagnostics (before Diags) is what would make "once" an assertion rather than a name.
There was a problem hiding this comment.
Fixed in 51148c3. TestLower_ListWithoutItems now counts what LowerComponentSchemas returns, before Diags, so a guard that appends twice reddens it. I planted exactly that and it went red, in that test only. Every row in the table builds each node once. The rebuild path has its own test, TestLower_ListWithoutItemsRebuiltWarnsOnce. It runs the #/components/schemas/Holder/properties/tags $ref in both orders and asserts the one warning the caller sees. Disabling Diags deduplication reddens its ref-first subtest only.
| // TestArrayItemsRequired_TheZeroContextReadsAsOptional completes the set the | ||
| // readers above start: every one answers on a context with nothing in it, and | ||
| // this one reaches through the document pointer like its neighbour, so it is | ||
| // the other one that would panic instead. |
There was a problem hiding this comment.
This comment is a copy of the neighbour's with the referents mangled: "the other one that would panic instead" has no referent (this accessor reaches through the document pointer exactly as ExclusiveBoundIsBoolean does), and "completes the set the readers above start" was already said about that test. Something that says what the test pins:
| // TestArrayItemsRequired_TheZeroContextReadsAsOptional completes the set the | |
| // readers above start: every one answers on a context with nothing in it, and | |
| // this one reaches through the document pointer like its neighbour, so it is | |
| // the other one that would panic instead. | |
| // TestArrayItemsRequired_TheZeroContextReadsAsOptional pins that a context | |
| // with no document answers false rather than panicking. |
There was a problem hiding this comment.
Rewritten in 491c471. The test is now TestIsOpenAPI30_TheZeroContextReadsAs2020, and its comment says what it pins: no document answers false rather than panicking.
| // alone, while 2020-12 (3.1 and 3.2) leaves it optional; a lowering that | ||
| // reported the wrong way would either warn on every legal 3.1 array or stay | ||
| // silent on the 3.0 defect this accessor exists to report. | ||
| func TestArrayItemsRequired_FollowsTheDialect(t *testing.T) { |
There was a problem hiding this comment.
This test, its table (the same six versions, in the same order) and the zero-context test above are clones of the ExclusiveBoundIsBoolean pair. If both accessors share one helper (see the note on lowering.go), one table can cover the dialect question and each accessor needs only its own one-line test.
There was a problem hiding this comment.
With the single accessor, one table (TestIsOpenAPI30_FollowsTheDialect) and one zero-context test cover the dialect question, and the clones are gone (491c471).
ExclusiveBoundIsBoolean and ArrayItemsRequired were the same two lines, a version re-parse and a "3.0" comparison, each with its own copy of the explanation and its own test table. Every later 3.0-ism would have added another method to Ctx, which sits near the 20-method cap. Both become Ctx.IsOpenAPI30, one accessor tested by one table, and Ctx is back to the method count it had before the missing-items warning. Its doc comment now states the limit both readers share: the dialect is the entry document's, so a 3.1 file reached by $ref from a 3.0 root is read as 3.0. The missing-items guard tests the nil items first, so the version is parsed only for the arrays that could warn. Its comment no longer claims the build runs once per pointer: a declaration a $ref reached first is built again by Types.InternDeclared, and the single warning the caller sees comes from compile.Diags dropping the identical second report. MissingArrayItems's doc comment is cut to fit the 100-word cap; the severity reasoning lives in the change that introduced it.
TestLower_ListWithoutItems collected its warnings through compile.Diags, which drops identical diagnostics, so a guard that reported twice left every "warns once" row green. The rows now count what the lowering returns, and a doubled append reddens them. A row pins the variant pointer under `type: [array, string]`. The one path that does report twice, a $ref naming a property's coordinate declared before its owner, gets its own test over both orders, asserting the single warning the caller sees. The corpus had no 3.0 document with an array, so the order-invariance sweep never reached the warning. missing-array-items.yaml declares the coordinate $ref first and reports at a component, a property and a query parameter, beside two arrays that write items and stay silent.
|
On the notes not attached to a line:
|
The comment on the missing-items guard names the 3.0 prefixItems case as a separate defect; it is now tracked as #820.
IsOpenAPI30's doc comment said every 3.0-ism asks it, but `nullable` is read in every dialect without asking, and whether it should be is #632. The comment now says which readings ask and names that exception, and cites #839 for the entry-document limit it already stated. TestIsOpenAPI30_FollowsTheDialect's comment drops the same claim, and says the reverse misreading warns on the 3.1 arrays that omit items, not on every 3.1 array. The rebuild test's comment tied the double report to a property's coordinate. A $ref to an `items` or to a parameter's schema, lowered before the declaration holding it, runs the guard twice the same way, so the comment names the mechanism rather than the one instance the test builds.
…equired params.go takes main's side: main moved the parameter's constraint read into schema.ConstraintsOf, which now asks IsOpenAPI30 where the branch asked it inline. Since32's comment named ExclusiveBoundIsBoolean, which this branch renamed, and the missing-array-items golden is regenerated for main's IR version and parameter IDs.
Main added Ctx.Since32 beside this branch's IsOpenAPI30, so the context now asks two dialect questions in two grammars. IsOpenAPI30 becomes Is30, the form its neighbour set, and its tests take Since32's test names.
The check sat in lowerArray's List branch, so it saw only the arrays that lower to a List. A 3.0 schema writing type array and no items that lowers to anything else was silent: one with prefixItems (a Tuple), a non-scalar or empty enum, a const, an allOf or oneOf beside it. A type list holding array warned at its variant pointer rather than at the schema. All of them break the same 3.0 rule. The check now runs in schemaBody, the step every non-reference schema body passes through above the dispatch that picks its lowering, so it keys on the declaration rather than on the node built for it. A $ref site never reaches it, which is right for 3.0, where what is written beside a $ref is ignored. A subschema kept verbatim, as under not, is never read as a schema and is not held to it, the same boundary the 3.0 exclusive-bound check has. The message no longer names an element type, which a tuple, an enum or a literal does not have. The conformance case gains an enum of arrays and now asserts every report the document draws, not only the expected ones.
…equired Main's IR version moved to 0.12.0, so the missing-array-items golden this branch adds is regenerated; nothing else in it changes.
TestCompile_MissingArrayItemsReachesTheDiagnostics filtered, counted and checked the severity and pointer of the warning by hand, which is what missingItemsAt already does for the lowering's own tests, and repeated a non-nil check parseFull makes. It now compares the pointer list the helper returns, which also fails on a second report.
Summary
OpenAPI 3.0 requires
itemson a schema whosetypeisarray. JSON Schema 2020-12, which 3.1 and 3.2 use, makes it optional. A 3.0 array schema that writes noitemslowered without a word: the document declares an array and never says what its elements are, and nothing tells the caller. This change reports it once per declaration, as a warning, at the schema's own pointer. Every type the IR holds is byte-identical to before, and the document only gains the warning. The element type was already the top type, so nothing the document declared is dropped.Rule. In a 3.0 document, a schema whose
typeis or includesarrayand which writes noitemsreports oneopenapi/missing-array-itemswarning. 3.1 and 3.2 are silent, and so is a 3.0 schema that wroteitems, includingitems: {}. The check keys on the keyword's absence, never on what the element lowered to.Where it runs: the declaration, not one lowering of it. The check sits in
schemaBody. Every non-reference schema body passes through it, above the dispatch that picks the lowering. So the rule reaches every array declaration whatever it lowers to:prefixItems;enum;const;allOforoneOfbeside it builds;array, reported at the schema rather than at a variant.Two alternatives were tried and rejected:
lowerArray. This PR's earlier revision put the check there. It missed every array that lowers to anything else. A sweep of 1,613 real-world 3.0 specs found one such array, a non-scalarenumwithtype: array, which drew no warning there.exclusiveMinimum, which the lowering checks. It also needs a second site for schemas in other documents, which the source walk never reaches. The duplicate-parameter rule needed that second site too, in the lowering.Boundaries, each deliberate.
$refsite is silent. It never reachesschemaBody, and 3.0 ignores whatever is written beside a$ref, so{$ref: …, type: array}declares no array.not, an inlineallOfbranch, a union kept beside a body, and the schema of a component parameter or header that nothing references. The 3.0 exclusive-bound check has the same boundary: onmain,not: {type: integer, exclusiveMinimum: 5}reports nothing, and neither does that bound in an unreferenced component parameter or header.$reffrom a 3.0 root is read as 3.0, and the reverse; that is openapi: a schema in another document is read in the root's dialect #839. A schema in another document is reported at each source$refthat reaches it rather than once, asmainalready reports an exclusive bound there.prefixItemsstill lowers to a tuple, which is openapi: a 3.0 document's prefixItems lowers to a tuple with no diagnostic #820. It now draws this warning aboutitems, which is accurate. openapi: a 3.0 document's prefixItems lowers to a tuple with no diagnostic #820's own defect, a 2020-12 keyword read in 3.0 without a word, is untouched.Severity: warning. Nothing declared is dropped, and this is a document defect that still lowers. That is the same shape
ReservedHeaderName,UnknownObjectKeyandEmptyEnumare warnings for indiag.go. An error would also stopharness.Checkat the first one and hide every later finding in the spec. It is a code of its own rather thanDegradedConstruct, because a code is what CI allowlists a finding by (docs/architecture.md§4) and nothing is kept in a weaker shape.Pointer: the schema's own. The missing keyword has no node in the source, so no
/itemsposition exists to name. Sibling missing-construct findings locate at the schema (EmptyEnum).The dialect question.
lowering.Ctx.Is30replacesExclusiveBoundIsBoolean, and both dialect readings ask it. It is named afterCtx.Since32, the other dialect predicate onCtx. An unrecognized version, and a zero-value context, answer false.nullableis still read in every dialect without asking, and whether it should be is #632.Lowered twice, reported once. A declaration that a
$refto one of its coordinates reaches first is lowered again when the declaration itself is lowered (Types.InternDeclared), and the check reports both times. The caller sees one warning, becausecompile.Diagsdrops identical diagnostics.Closes #650.
Test plan
TestIs30_FollowsTheDeclaredVersion(3.0.0 and 3.0.3 true; 3.1.0, 3.2.0,4.0.0and""false) andTestIs30_TheZeroContextReadsAs2020.TestLower_ListWithoutItems: a bare array lowers to a List overt/prim/anyin 3.0.3, 3.1.0 and 3.2.0 alike.TestLower_MissingArrayItemscounts what the lowering returns, beforecompile.Diagsdeduplicates.items, a type list ([array, string], at the schema), a tuple, an enum of arrays, an empty enum, aconst, anallOfbeside it, aoneOfbeside it, and a component a sibling's$refnames.items: {}, array keywords without atype({minItems: 1}), a$refsite's siblingtype: array, an array undernot, and 3.1.0 and 3.2.0.TestLower_MissingArrayItemsLoweredTwiceWarnsOncecovers the coordinate-$refpath in both orders. It asserts the raw report count (2 ref-first, 1 declaration-first), so the test is known to reach the double lowering, and one warning aftercompile.Diags.TestCompile_MissingArrayItemsReachesTheDiagnosticscompiles the issue's exact document end to end. It asserts that exactly one warning reachesCompile's diagnostics, at the schema's pointer and as a warning, and that the document still holds a List over the top type.missing-array-items(3.0.3, with the coordinate$refdeclared before its target) asserts the full set of reports the document draws: a component, an enum of arrays, a property and a query parameter. Two arrays that write items stay silent. It is the only golden added or changed.diag_test.go'scodes()lists the new code.schemaBodyreddens every reporting row, the double-lowering test and the conformance case.lowerArray's List branch only reddens the tuple, enum of arrays, empty enum,const,allOfand type-list rows, and the conformance case.typeconjunct, or theitemsconjunct, reddens the silent rows it guards.Is30to 3.2 reddens its table and the 3.2 row.arraytype reddens the type-list row.compile.Diagsdeduplication for this code reddens the ref-first double-lowering row and the conformance case.main's binary and this branch's:type: arraymappings withoutitemsin their sources, and each warning's pointer was checked against its source.main.make gate(fmt, vet, lint, build, coverage, fuzz, bench-smoke) green at 3309b1e, with coverage a 100% statement gate.