@@ -2330,6 +2330,90 @@ func TestOneOf_CoDeclaredNotDistributedReasons(t *testing.T) {
23302330 "each declined shape is reported once; got %+v" , diags )
23312331}
23322332
2333+ // TestUnionCombinators_CoDeclaredKeepsTheBoundsWrittenBesideIt pins that keeping
2334+ // a union verbatim does not cost the position the value constraints written
2335+ // beside it. The alias exists so the union attaches to a node this pointer owns
2336+ // rather than to the shared primitive the body reduced to, and owning a node is
2337+ // what stops hoistDeclarationHome hoisting the alias that would otherwise carry
2338+ // the bounds — so this alias has to carry them itself, as every other hoist here
2339+ // does (GitHub #343).
2340+ //
2341+ // Both reasons that keep a union hoist the same alias, and anyOf rides the same
2342+ // path as oneOf, so each is covered. The last case pins what reading the bounds
2343+ // also produces: the co-declared-bound reconciliation reports at a position
2344+ // nothing used to read, and the keyword it cannot home is kept on the node the
2345+ // bounds landed on. The key set is asserted whole — carrying the constraints
2346+ // without that keyword leaves the diagnostic naming an entry the node lacks.
2347+ func TestUnionCombinators_CoDeclaredKeepsTheBoundsWrittenBesideIt (t * testing.T ) {
2348+ t .Parallel ()
2349+ three := int64 (3 )
2350+ ten , five := ir .BigVal ("10" ), ir .BigVal ("5" )
2351+ cases := []struct {
2352+ name , schemas , unionKey string
2353+ reason ir.UnmodeledReason
2354+ want ir.Constraints
2355+ wantKept []string
2356+ wantDiag string
2357+ }{
2358+ {
2359+ name : "a validation-only union" ,
2360+ schemas : " A: {type: string, minLength: 3, oneOf: [{minLength: 1}, {minLength: 2}]}\n " ,
2361+ unionKey : "openapi:oneOf" ,
2362+ reason : ir .ReasonValidationOnly ,
2363+ want : ir.Constraints {MinLength : & three },
2364+ wantKept : []string {"openapi:oneOf" },
2365+ },
2366+ {
2367+ name : "a union kept as a degraded lowering" ,
2368+ schemas : " A: {type: number, minimum: 10, multipleOf: 5, oneOf: [{type: string}, {type: integer}]}\n " ,
2369+ unionKey : "openapi:oneOf" ,
2370+ reason : ir .ReasonDegradedLowering ,
2371+ want : ir.Constraints {Min : & ten , MultipleOf : & five },
2372+ wantKept : []string {"openapi:oneOf" },
2373+ },
2374+ {
2375+ name : "an anyOf kept in place of a oneOf" ,
2376+ schemas : " A: {type: string, minLength: 3, anyOf: [{minLength: 1}, {minLength: 2}]}\n " ,
2377+ unionKey : "openapi:anyOf" ,
2378+ reason : ir .ReasonValidationOnly ,
2379+ want : ir.Constraints {MinLength : & three },
2380+ wantKept : []string {"openapi:anyOf" },
2381+ },
2382+ {
2383+ name : "co-declared bounds beside a union" ,
2384+ schemas : " A: {type: number, minimum: 10, exclusiveMinimum: 0, oneOf: [{minLength: 1}, {minLength: 2}]}\n " ,
2385+ unionKey : "openapi:oneOf" ,
2386+ reason : ir .ReasonValidationOnly ,
2387+ want : ir.Constraints {Min : & ten },
2388+ wantKept : []string {"openapi:exclusiveMinimum" , "openapi:oneOf" },
2389+ wantDiag : "kept minimum as the tighter of the two" ,
2390+ },
2391+ }
2392+ for _ , tc := range cases {
2393+ t .Run (tc .name , func (t * testing.T ) {
2394+ t .Parallel ()
2395+ doc , diags := lowerSpec (t , openapitest .ComponentSpec (tc .schemas ))
2396+ openapitest .RequireNoErrorDiags (t , diags )
2397+
2398+ sc , ok := typeByName (doc , "A" ).(* ir.Scalar )
2399+ require .True (t , ok , "the preserved union hoists an alias over the shared primitive" )
2400+ entry , ok := sc .Unmodeled [tc .unionKey ]
2401+ require .True (t , ok , "and keeps the union on it" )
2402+ assert .Equal (t , tc .reason , entry .Reason )
2403+ require .NotNil (t , sc .Constraints , "while keeping the bounds written beside it" )
2404+ assert .Empty (t , cmp .Diff (tc .want , * sc .Constraints ))
2405+ assert .Equal (t , tc .wantKept , unmodeledKeys (sc .Unmodeled ),
2406+ "the bound keyword that reaches no Constraints field is kept on the same node" )
2407+ if tc .wantDiag == "" {
2408+ return
2409+ }
2410+ assert .Contains (t ,
2411+ openapitest .DiagMessageAt (t , diags , diag .DegradedConstruct , ir .SeverityInfo , "/components/schemas/A" ),
2412+ tc .wantDiag , "reading the bounds is what reports on them" )
2413+ })
2414+ }
2415+ }
2416+
23332417// TestUnionCombinators_PassedOverBranchSetIsKept covers the preference nothing
23342418// used to record (GitHub #35). unionBranches takes oneOf whenever it is written
23352419// and falls back to anyOf only when it is not, so a schema declaring both lost
0 commit comments