@@ -39,11 +39,15 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
3939 return results
4040 }
4141
42- // cache to prevent checking the same schema twice
42+ // in OAS 3.0.x, $ref siblings are ignored, so allOf is the only way to add properties
43+ isOAS30 := false
44+ if context .SpecInfo != nil {
45+ isOAS30 = context .SpecInfo .VersionNumeric >= 3.0 && context .SpecInfo .VersionNumeric < 3.1
46+ }
47+
4348 seen := make (map [string ]bool )
4449
4550 buildResult := func (message , path string , node * yaml.Node , component v3.AcceptsRuleResults ) model.RuleFunctionResult {
46- // try to find all paths for this node if it's a schema
4751 var allPaths []string
4852 if schema , ok := component .(* v3.Schema ); ok {
4953 _ , allPaths = vacuumUtils .LocateSchemaPropertyPaths (context , schema , node , node )
@@ -57,7 +61,6 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
5761 Rule : context .Rule ,
5862 }
5963
60- // set the Paths array if we found multiple locations
6164 if len (allPaths ) > 1 {
6265 result .Paths = allPaths
6366 }
@@ -69,6 +72,12 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
6972 checkCombinator := func (schema * v3.Schema , combinatorName string , combinatorSlice []* libopenapi_base.SchemaProxy ,
7073 keyNode * yaml.Node ) {
7174 if len (combinatorSlice ) == 1 {
75+ // OAS 3.0.x: allOf with single $ref + sibling properties is legitimate
76+ if isOAS30 && combinatorName == "allOf" &&
77+ hasSiblingProperties (schema ) && hasRefInCombinator (combinatorSlice ) {
78+ return
79+ }
80+
7281 path := fmt .Sprintf ("%s.%s" , schema .GenerateJSONPath (), combinatorName )
7382 message := fmt .Sprintf ("schema with `%s` combinator containing only one item " +
7483 "should be replaced with the item directly" , combinatorName )
@@ -82,7 +91,6 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
8291 return
8392 }
8493
85- // create cache key to prevent duplicate processing
8694 var cacheKey strings.Builder
8795 cacheKey .WriteString (schema .GenerateJSONPath ())
8896 key := cacheKey .String ()
@@ -92,23 +100,19 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
92100 }
93101 seen [key ] = true
94102
95- // check allOf combinator
96103 if schema .Value .AllOf != nil && len (schema .Value .AllOf ) > 0 {
97104 checkCombinator (schema , "allOf" , schema .Value .AllOf , schema .Value .GoLow ().AllOf .GetKeyNode ())
98105 }
99106
100- // check anyOf combinator
101107 if schema .Value .AnyOf != nil && len (schema .Value .AnyOf ) > 0 {
102108 checkCombinator (schema , "anyOf" , schema .Value .AnyOf , schema .Value .GoLow ().AnyOf .GetKeyNode ())
103109 }
104110
105- // check oneOf combinator
106111 if schema .Value .OneOf != nil && len (schema .Value .OneOf ) > 0 {
107112 checkCombinator (schema , "oneOf" , schema .Value .OneOf , schema .Value .GoLow ().OneOf .GetKeyNode ())
108113 }
109114 }
110115
111- // check all schemas in the document
112116 if context .DrDocument .Schemas != nil {
113117 for i := range context .DrDocument .Schemas {
114118 checkSchema (context .DrDocument .Schemas [i ])
@@ -117,3 +121,39 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
117121
118122 return results
119123}
124+
125+ func hasSiblingProperties (schema * v3.Schema ) bool {
126+ if schema == nil || schema .Value == nil {
127+ return false
128+ }
129+ v := schema .Value
130+
131+ if v .Description != "" || v .Title != "" {
132+ return true
133+ }
134+ if v .Default != nil || v .Example != nil || v .ExternalDocs != nil {
135+ return true
136+ }
137+ if v .Nullable != nil || v .ReadOnly != nil || v .WriteOnly != nil || v .Deprecated != nil {
138+ return true
139+ }
140+ if v .XML != nil {
141+ return true
142+ }
143+ if len (v .Enum ) > 0 {
144+ return true
145+ }
146+ lowSchema := v .GoLow ()
147+ if lowSchema != nil && lowSchema .Extensions != nil && lowSchema .Extensions .Len () > 0 {
148+ return true
149+ }
150+ return false
151+ }
152+
153+ func hasRefInCombinator (combinatorSlice []* libopenapi_base.SchemaProxy ) bool {
154+ if len (combinatorSlice ) != 1 || combinatorSlice [0 ] == nil {
155+ return false
156+ }
157+ lowProxy := combinatorSlice [0 ].GoLow ()
158+ return lowProxy != nil && lowProxy .GetReference () != ""
159+ }
0 commit comments