Skip to content

Commit a241e1c

Browse files
committed
RFC: Return type overlap validation
Implements graphql/graphql-spec#162 This alters the "Overlapping Fields Can Be Merged" validation rule to better express return type validation issues. The existing rule has presented some false-positives in some schema where interfaces with co-variant types are commonly used. The "same return type" check doesn't remove any ambiguity. Instead what that "same return type" check is attempting to prevent is spreading two fragments (or inline fragments) which have fields with return types where ambiguity would be introduced in the response. In order to curb false-positives, we later changed this rule such that if two fields were known to never apply simultaneously, then we would skip the remainder of the rule. ``` { ... on Person { foo: fullName } ... on Pet { foo: petName } } ``` However this can introduce false-negatives! ``` { ... on Person { foo: birthday { bar: year } } ... on Business { foo: location { bar: street } } } ``` In the above example, `data.foo.bar` could be of type `Int` or type `String`, it's ambiguous! This differing return type breaks some client model implementations (Fragment models) in addition to just being confusing. Commit: c034de91acce10d5c06d03bd332c6ebd45e2213c [c034de9] Parents: ffe76c51c4 Author: Lee Byron <[email protected]> Date: 7 April 2016 at 2:00:31 PM SGT Labels: HEAD
1 parent cec8092 commit a241e1c

File tree

2 files changed

+354
-97
lines changed

2 files changed

+354
-97
lines changed

rules.go

Lines changed: 133 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,16 +1245,53 @@ func sameType(typeA, typeB Type) bool {
12451245
return false
12461246
}
12471247

1248+
// Two types conflict if both types could not apply to a value simultaneously.
1249+
// Composite types are ignored as their individual field types will be compared
1250+
// later recursively. However List and Non-Null types must match.
1251+
func doTypesConflict(type1 Output, type2 Output) bool {
1252+
if type1, ok := type1.(*List); ok {
1253+
if type2, ok := type2.(*List); ok {
1254+
return doTypesConflict(type1.OfType, type2.OfType)
1255+
}
1256+
return true
1257+
}
1258+
if type2, ok := type2.(*List); ok {
1259+
if type1, ok := type1.(*List); ok {
1260+
return doTypesConflict(type1.OfType, type2.OfType)
1261+
}
1262+
return true
1263+
}
1264+
if type1, ok := type1.(*NonNull); ok {
1265+
if type2, ok := type2.(*NonNull); ok {
1266+
return doTypesConflict(type1.OfType, type2.OfType)
1267+
}
1268+
return true
1269+
}
1270+
if type2, ok := type2.(*NonNull); ok {
1271+
if type1, ok := type1.(*NonNull); ok {
1272+
return doTypesConflict(type1.OfType, type2.OfType)
1273+
}
1274+
return true
1275+
}
1276+
if IsLeafType(type1) || IsLeafType(type2) {
1277+
return type1 != type2
1278+
}
1279+
return false
1280+
}
1281+
12481282
// OverlappingFieldsCanBeMergedRule Overlapping fields can be merged
12491283
//
12501284
// A selection set is only valid if all fields (including spreading any
12511285
// fragments) either correspond to distinct response names or can be merged
12521286
// without ambiguity.
12531287
func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRuleInstance {
12541288

1289+
var getSubfieldMap func(ast1 *ast.Field, type1 Output, ast2 *ast.Field, type2 Output) map[string][]*fieldDefPair
1290+
var subfieldConflicts func(conflicts []*conflict, responseName string, ast1 *ast.Field, ast2 *ast.Field) *conflict
1291+
var findConflicts func(parentFieldsAreMutuallyExclusive bool, fieldMap map[string][]*fieldDefPair) (conflicts []*conflict)
1292+
12551293
comparedSet := newPairSet()
1256-
var findConflicts func(fieldMap map[string][]*fieldDefPair) (conflicts []*conflict)
1257-
findConflict := func(responseName string, field *fieldDefPair, field2 *fieldDefPair) *conflict {
1294+
findConflict := func(parentFieldsAreMutuallyExclusive bool, responseName string, field *fieldDefPair, field2 *fieldDefPair) *conflict {
12581295

12591296
parentType1 := field.ParentType
12601297
ast1 := field.Field
@@ -1269,46 +1306,21 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul
12691306
return nil
12701307
}
12711308

1272-
// If the statically known parent types could not possibly apply at the same
1273-
// time, then it is safe to permit them to diverge as they will not present
1274-
// any ambiguity by differing.
1275-
// It is known that two parent types could never overlap if they are
1276-
// different Object types. Interface or Union types might overlap - if not
1277-
// in the current state of the schema, then perhaps in some future version,
1278-
// thus may not safely diverge.
1279-
if parentType1 != parentType2 {
1280-
_, ok1 := parentType1.(*Object)
1281-
_, ok2 := parentType2.(*Object)
1282-
if ok1 && ok2 {
1283-
return nil
1284-
}
1285-
}
1286-
12871309
// Memoize, do not report the same issue twice.
1310+
// Note: Two overlapping ASTs could be encountered both when
1311+
// `parentFieldsAreMutuallyExclusive` is true and is false, which could
1312+
// produce different results (when `true` being a subset of `false`).
1313+
// However we do not need to include this piece of information when
1314+
// memoizing since this rule visits leaf fields before their parent fields,
1315+
// ensuring that `parentFieldsAreMutuallyExclusive` is `false` the first
1316+
// time two overlapping fields are encountered, ensuring that the full
1317+
// set of validation rules are always checked when necessary.
12881318
if comparedSet.Has(ast1, ast2) {
12891319
return nil
12901320
}
12911321
comparedSet.Add(ast1, ast2)
12921322

1293-
name1 := ""
1294-
if ast1.Name != nil {
1295-
name1 = ast1.Name.Value
1296-
}
1297-
name2 := ""
1298-
if ast2.Name != nil {
1299-
name2 = ast2.Name.Value
1300-
}
1301-
if name1 != name2 {
1302-
return &conflict{
1303-
Reason: conflictReason{
1304-
Name: responseName,
1305-
Message: fmt.Sprintf(`%v and %v are different fields`, name1, name2),
1306-
},
1307-
FieldsLeft: []ast.Node{ast1},
1308-
FieldsRight: []ast.Node{ast2},
1309-
}
1310-
}
1311-
1323+
// The return type for each field.
13121324
var type1 Type
13131325
var type2 Type
13141326
if def1 != nil {
@@ -1318,27 +1330,74 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul
13181330
type2 = def2.Type
13191331
}
13201332

1321-
if type1 != nil && type2 != nil && !sameType(type1, type2) {
1322-
return &conflict{
1323-
Reason: conflictReason{
1324-
Name: responseName,
1325-
Message: fmt.Sprintf(`they return differing types %v and %v`, type1, type2),
1326-
},
1327-
FieldsLeft: []ast.Node{ast1},
1328-
FieldsRight: []ast.Node{ast2},
1333+
// If it is known that two fields could not possibly apply at the same
1334+
// time, due to the parent types, then it is safe to permit them to diverge
1335+
// in aliased field or arguments used as they will not present any ambiguity
1336+
// by differing.
1337+
// It is known that two parent types could never overlap if they are
1338+
// different Object types. Interface or Union types might overlap - if not
1339+
// in the current state of the schema, then perhaps in some future version,
1340+
// thus may not safely diverge.
1341+
_, isParentType1Object := parentType1.(*Object)
1342+
_, isParentType2Object := parentType2.(*Object)
1343+
fieldsAreMutuallyExclusive := parentFieldsAreMutuallyExclusive || parentType1 != parentType2 && isParentType1Object && isParentType2Object
1344+
1345+
if !fieldsAreMutuallyExclusive {
1346+
// Two aliases must refer to the same field.
1347+
name1 := ""
1348+
name2 := ""
1349+
1350+
if ast1.Name != nil {
1351+
name1 = ast1.Name.Value
1352+
}
1353+
if ast2.Name != nil {
1354+
name2 = ast2.Name.Value
1355+
}
1356+
if name1 != name2 {
1357+
return &conflict{
1358+
Reason: conflictReason{
1359+
Name: responseName,
1360+
Message: fmt.Sprintf(`%v and %v are different fields`, name1, name2),
1361+
},
1362+
FieldsLeft: []ast.Node{ast1},
1363+
FieldsRight: []ast.Node{ast2},
1364+
}
1365+
}
1366+
1367+
// Two field calls must have the same arguments.
1368+
if !sameArguments(ast1.Arguments, ast2.Arguments) {
1369+
return &conflict{
1370+
Reason: conflictReason{
1371+
Name: responseName,
1372+
Message: `they have differing arguments`,
1373+
},
1374+
FieldsLeft: []ast.Node{ast1},
1375+
FieldsRight: []ast.Node{ast2},
1376+
}
13291377
}
13301378
}
1331-
if !sameArguments(ast1.Arguments, ast2.Arguments) {
1379+
1380+
if type1 != nil && type2 != nil && doTypesConflict(type1, type2) {
13321381
return &conflict{
13331382
Reason: conflictReason{
13341383
Name: responseName,
1335-
Message: `they have differing arguments`,
1384+
Message: fmt.Sprintf(`they return conflicting types %v and %v`, type1, type2),
13361385
},
13371386
FieldsLeft: []ast.Node{ast1},
13381387
FieldsRight: []ast.Node{ast2},
13391388
}
13401389
}
13411390

1391+
subFieldMap := getSubfieldMap(ast1, type1, ast2, type2)
1392+
if subFieldMap != nil {
1393+
conflicts := findConflicts(fieldsAreMutuallyExclusive, subFieldMap)
1394+
return subfieldConflicts(conflicts, responseName, ast1, ast2)
1395+
}
1396+
1397+
return nil
1398+
}
1399+
1400+
getSubfieldMap = func(ast1 *ast.Field, type1 Output, ast2 *ast.Field, type2 Output) map[string][]*fieldDefPair {
13421401
selectionSet1 := ast1.SelectionSet
13431402
selectionSet2 := ast2.SelectionSet
13441403
if selectionSet1 != nil && selectionSet2 != nil {
@@ -1357,32 +1416,34 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul
13571416
visitedFragmentNames,
13581417
subfieldMap,
13591418
)
1360-
conflicts := findConflicts(subfieldMap)
1361-
if len(conflicts) > 0 {
1362-
1363-
conflictReasons := []conflictReason{}
1364-
conflictFieldsLeft := []ast.Node{ast1}
1365-
conflictFieldsRight := []ast.Node{ast2}
1366-
for _, c := range conflicts {
1367-
conflictReasons = append(conflictReasons, c.Reason)
1368-
conflictFieldsLeft = append(conflictFieldsLeft, c.FieldsLeft...)
1369-
conflictFieldsRight = append(conflictFieldsRight, c.FieldsRight...)
1370-
}
1419+
return subfieldMap
1420+
}
1421+
return nil
1422+
}
13711423

1372-
return &conflict{
1373-
Reason: conflictReason{
1374-
Name: responseName,
1375-
Message: conflictReasons,
1376-
},
1377-
FieldsLeft: conflictFieldsLeft,
1378-
FieldsRight: conflictFieldsRight,
1379-
}
1424+
subfieldConflicts = func(conflicts []*conflict, responseName string, ast1 *ast.Field, ast2 *ast.Field) *conflict {
1425+
if len(conflicts) > 0 {
1426+
conflictReasons := []conflictReason{}
1427+
conflictFieldsLeft := []ast.Node{ast1}
1428+
conflictFieldsRight := []ast.Node{ast2}
1429+
for _, c := range conflicts {
1430+
conflictReasons = append(conflictReasons, c.Reason)
1431+
conflictFieldsLeft = append(conflictFieldsLeft, c.FieldsLeft...)
1432+
conflictFieldsRight = append(conflictFieldsRight, c.FieldsRight...)
1433+
}
1434+
1435+
return &conflict{
1436+
Reason: conflictReason{
1437+
Name: responseName,
1438+
Message: conflictReasons,
1439+
},
1440+
FieldsLeft: conflictFieldsLeft,
1441+
FieldsRight: conflictFieldsRight,
13801442
}
13811443
}
13821444
return nil
13831445
}
1384-
1385-
findConflicts = func(fieldMap map[string][]*fieldDefPair) (conflicts []*conflict) {
1446+
findConflicts = func(parentFieldsAreMutuallyExclusive bool, fieldMap map[string][]*fieldDefPair) (conflicts []*conflict) {
13861447

13871448
// ensure field traversal
13881449
orderedName := sort.StringSlice{}
@@ -1395,7 +1456,7 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul
13951456
fields, _ := fieldMap[responseName]
13961457
for _, fieldA := range fields {
13971458
for _, fieldB := range fields {
1398-
c := findConflict(responseName, fieldA, fieldB)
1459+
c := findConflict(parentFieldsAreMutuallyExclusive, responseName, fieldA, fieldB)
13991460
if c != nil {
14001461
conflicts = append(conflicts, c)
14011462
}
@@ -1429,6 +1490,9 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul
14291490
visitorOpts := &visitor.VisitorOptions{
14301491
KindFuncMap: map[string]visitor.NamedVisitFuncs{
14311492
kinds.SelectionSet: visitor.NamedVisitFuncs{
1493+
// Note: we validate on the reverse traversal so deeper conflicts will be
1494+
// caught first, for correct calculation of mutual exclusivity and for
1495+
// clearer error messages.
14321496
Leave: func(p visitor.VisitFuncParams) (string, interface{}) {
14331497
if selectionSet, ok := p.Node.(*ast.SelectionSet); ok && selectionSet != nil {
14341498
parentType, _ := context.ParentType().(Named)
@@ -1439,7 +1503,7 @@ func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRul
14391503
nil,
14401504
nil,
14411505
)
1442-
conflicts := findConflicts(fieldMap)
1506+
conflicts := findConflicts(false, fieldMap)
14431507
if len(conflicts) > 0 {
14441508
for _, c := range conflicts {
14451509
responseName := c.Reason.Name

0 commit comments

Comments
 (0)