Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
2 contributors

Users who have contributed to this file

@chris-ramon @sogko
707 lines (631 sloc) 24.7 KB
package graphql
import (
"fmt"
"strings"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/kinds"
"github.com/graphql-go/graphql/language/printer"
"github.com/graphql-go/graphql/language/visitor"
)
func fieldsConflictMessage(responseName string, reason conflictReason) string {
return fmt.Sprintf(`Fields "%v" conflict because %v. `+
`Use different aliases on the fields to fetch both if this was intentional.`,
responseName,
fieldsConflictReasonMessage(reason),
)
}
func fieldsConflictReasonMessage(message interface{}) string {
switch reason := message.(type) {
case string:
return reason
case conflictReason:
return fieldsConflictReasonMessage(reason.Message)
case []conflictReason:
messages := []string{}
for _, r := range reason {
messages = append(messages, fmt.Sprintf(
`subfields "%v" conflict because %v`,
r.Name,
fieldsConflictReasonMessage(r.Message),
))
}
return strings.Join(messages, " and ")
}
return ""
}
// OverlappingFieldsCanBeMergedRule Overlapping fields can be merged
//
// A selection set is only valid if all fields (including spreading any
// fragments) either correspond to distinct response names or can be merged
// without ambiguity.
func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRuleInstance {
// A memoization for when two fragments are compared "between" each other for
// conflicts. Two fragments may be compared many times, so memoizing this can
// dramatically improve the performance of this validator.
comparedSet := newPairSet()
// A cache for the "field map" and list of fragment names found in any given
// selection set. Selection sets may be asked for this information multiple
// times, so this improves the performance of this validator.
cacheMap := map[*ast.SelectionSet]*fieldsAndFragmentNames{}
visitorOpts := &visitor.VisitorOptions{
KindFuncMap: map[string]visitor.NamedVisitFuncs{
kinds.SelectionSet: {
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
if selectionSet, ok := p.Node.(*ast.SelectionSet); ok && selectionSet != nil {
parentType, _ := context.ParentType().(Named)
rule := &overlappingFieldsCanBeMergedRule{
context: context,
comparedSet: comparedSet,
cacheMap: cacheMap,
}
conflicts := rule.findConflictsWithinSelectionSet(parentType, selectionSet)
if len(conflicts) > 0 {
for _, c := range conflicts {
responseName := c.Reason.Name
reason := c.Reason
reportError(
context,
fieldsConflictMessage(responseName, reason),
append(c.FieldsLeft, c.FieldsRight...),
)
}
return visitor.ActionNoChange, nil
}
}
return visitor.ActionNoChange, nil
},
},
},
}
return &ValidationRuleInstance{
VisitorOpts: visitorOpts,
}
}
/**
* Algorithm:
*
* Conflicts occur when two fields exist in a query which will produce the same
* response name, but represent differing values, thus creating a conflict.
* The algorithm below finds all conflicts via making a series of comparisons
* between fields. In order to compare as few fields as possible, this makes
* a series of comparisons "within" sets of fields and "between" sets of fields.
*
* Given any selection set, a collection produces both a set of fields by
* also including all inline fragments, as well as a list of fragments
* referenced by fragment spreads.
*
* A) Each selection set represented in the document first compares "within" its
* collected set of fields, finding any conflicts between every pair of
* overlapping fields.
* Note: This is the *only time* that a the fields "within" a set are compared
* to each other. After this only fields "between" sets are compared.
*
* B) Also, if any fragment is referenced in a selection set, then a
* comparison is made "between" the original set of fields and the
* referenced fragment.
*
* C) Also, if multiple fragments are referenced, then comparisons
* are made "between" each referenced fragment.
*
* D) When comparing "between" a set of fields and a referenced fragment, first
* a comparison is made between each field in the original set of fields and
* each field in the the referenced set of fields.
*
* E) Also, if any fragment is referenced in the referenced selection set,
* then a comparison is made "between" the original set of fields and the
* referenced fragment (recursively referring to step D).
*
* F) When comparing "between" two fragments, first a comparison is made between
* each field in the first referenced set of fields and each field in the the
* second referenced set of fields.
*
* G) Also, any fragments referenced by the first must be compared to the
* second, and any fragments referenced by the second must be compared to the
* first (recursively referring to step F).
*
* H) When comparing two fields, if both have selection sets, then a comparison
* is made "between" both selection sets, first comparing the set of fields in
* the first selection set with the set of fields in the second.
*
* I) Also, if any fragment is referenced in either selection set, then a
* comparison is made "between" the other set of fields and the
* referenced fragment.
*
* J) Also, if two fragments are referenced in both selection sets, then a
* comparison is made "between" the two fragments.
*
*/
type overlappingFieldsCanBeMergedRule struct {
context *ValidationContext
// A memoization for when two fragments are compared "between" each other for
// conflicts. Two fragments may be compared many times, so memoizing this can
// dramatically improve the performance of this validator.
comparedSet *pairSet
// A cache for the "field map" and list of fragment names found in any given
// selection set. Selection sets may be asked for this information multiple
// times, so this improves the performance of this validator.
cacheMap map[*ast.SelectionSet]*fieldsAndFragmentNames
}
// Find all conflicts found "within" a selection set, including those found
// via spreading in fragments. Called when visiting each SelectionSet in the
// GraphQL Document.
func (rule *overlappingFieldsCanBeMergedRule) findConflictsWithinSelectionSet(parentType Named, selectionSet *ast.SelectionSet) []conflict {
conflicts := []conflict{}
fieldsInfo := rule.getFieldsAndFragmentNames(parentType, selectionSet)
// (A) Find find all conflicts "within" the fields of this selection set.
// Note: this is the *only place* `collectConflictsWithin` is called.
conflicts = rule.collectConflictsWithin(conflicts, fieldsInfo)
// (B) Then collect conflicts between these fields and those represented by
// each spread fragment name found.
for i := 0; i < len(fieldsInfo.fragmentNames); i++ {
conflicts = rule.collectConflictsBetweenFieldsAndFragment(conflicts, false, fieldsInfo, fieldsInfo.fragmentNames[i])
// (C) Then compare this fragment with all other fragments found in this
// selection set to collect conflicts between fragments spread together.
// This compares each item in the list of fragment names to every other item
// in that same list (except for itself).
for k := i + 1; k < len(fieldsInfo.fragmentNames); k++ {
conflicts = rule.collectConflictsBetweenFragments(conflicts, false, fieldsInfo.fragmentNames[i], fieldsInfo.fragmentNames[k])
}
}
return conflicts
}
// Collect all conflicts found between a set of fields and a fragment reference
// including via spreading in any nested fragments.
func (rule *overlappingFieldsCanBeMergedRule) collectConflictsBetweenFieldsAndFragment(conflicts []conflict, areMutuallyExclusive bool, fieldsInfo *fieldsAndFragmentNames, fragmentName string) []conflict {
fragment := rule.context.Fragment(fragmentName)
if fragment == nil {
return conflicts
}
fieldsInfo2 := rule.getReferencedFieldsAndFragmentNames(fragment)
// (D) First collect any conflicts between the provided collection of fields
// and the collection of fields represented by the given fragment.
conflicts = rule.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsInfo, fieldsInfo2)
// (E) Then collect any conflicts between the provided collection of fields
// and any fragment names found in the given fragment.
for _, fragmentName2 := range fieldsInfo2.fragmentNames {
conflicts = rule.collectConflictsBetweenFieldsAndFragment(conflicts, areMutuallyExclusive, fieldsInfo2, fragmentName2)
}
return conflicts
}
// Collect all conflicts found between two fragments, including via spreading in
// any nested fragments.
func (rule *overlappingFieldsCanBeMergedRule) collectConflictsBetweenFragments(conflicts []conflict, areMutuallyExclusive bool, fragmentName1 string, fragmentName2 string) []conflict {
fragment1 := rule.context.Fragment(fragmentName1)
fragment2 := rule.context.Fragment(fragmentName2)
if fragment1 == nil || fragment2 == nil {
return conflicts
}
// No need to compare a fragment to itself.
if fragment1 == fragment2 {
return conflicts
}
// Memoize so two fragments are not compared for conflicts more than once.
if rule.comparedSet.Has(fragmentName1, fragmentName2, areMutuallyExclusive) {
return conflicts
}
rule.comparedSet.Add(fragmentName1, fragmentName2, areMutuallyExclusive)
fieldsInfo1 := rule.getReferencedFieldsAndFragmentNames(fragment1)
fieldsInfo2 := rule.getReferencedFieldsAndFragmentNames(fragment2)
// (F) First, collect all conflicts between these two collections of fields
// (not including any nested fragments).
conflicts = rule.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsInfo1, fieldsInfo2)
// (G) Then collect conflicts between the first fragment and any nested
// fragments spread in the second fragment.
for _, innerFragmentName2 := range fieldsInfo2.fragmentNames {
conflicts = rule.collectConflictsBetweenFragments(conflicts, areMutuallyExclusive, fragmentName1, innerFragmentName2)
}
// (G) Then collect conflicts between the second fragment and any nested
// fragments spread in the first fragment.
for _, innerFragmentName1 := range fieldsInfo1.fragmentNames {
conflicts = rule.collectConflictsBetweenFragments(conflicts, areMutuallyExclusive, innerFragmentName1, fragmentName2)
}
return conflicts
}
// Find all conflicts found between two selection sets, including those found
// via spreading in fragments. Called when determining if conflicts exist
// between the sub-fields of two overlapping fields.
func (rule *overlappingFieldsCanBeMergedRule) findConflictsBetweenSubSelectionSets(areMutuallyExclusive bool, parentType1 Named, selectionSet1 *ast.SelectionSet, parentType2 Named, selectionSet2 *ast.SelectionSet) []conflict {
conflicts := []conflict{}
fieldsInfo1 := rule.getFieldsAndFragmentNames(parentType1, selectionSet1)
fieldsInfo2 := rule.getFieldsAndFragmentNames(parentType2, selectionSet2)
// (H) First, collect all conflicts between these two collections of field.
conflicts = rule.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsInfo1, fieldsInfo2)
// (I) Then collect conflicts between the first collection of fields and
// those referenced by each fragment name associated with the second.
for _, fragmentName2 := range fieldsInfo2.fragmentNames {
conflicts = rule.collectConflictsBetweenFieldsAndFragment(conflicts, areMutuallyExclusive, fieldsInfo1, fragmentName2)
}
// (I) Then collect conflicts between the second collection of fields and
// those referenced by each fragment name associated with the first.
for _, fragmentName1 := range fieldsInfo1.fragmentNames {
conflicts = rule.collectConflictsBetweenFieldsAndFragment(conflicts, areMutuallyExclusive, fieldsInfo2, fragmentName1)
}
// (J) Also collect conflicts between any fragment names by the first and
// fragment names by the second. This compares each item in the first set of
// names to each item in the second set of names.
for _, fragmentName1 := range fieldsInfo1.fragmentNames {
for _, fragmentName2 := range fieldsInfo2.fragmentNames {
conflicts = rule.collectConflictsBetweenFragments(conflicts, areMutuallyExclusive, fragmentName1, fragmentName2)
}
}
return conflicts
}
// Collect all Conflicts "within" one collection of fields.
func (rule *overlappingFieldsCanBeMergedRule) collectConflictsWithin(conflicts []conflict, fieldsInfo *fieldsAndFragmentNames) []conflict {
// A field map is a keyed collection, where each key represents a response
// name and the value at that key is a list of all fields which provide that
// response name. For every response name, if there are multiple fields, they
// must be compared to find a potential conflict.
for _, responseName := range fieldsInfo.fieldsOrder {
fields, ok := fieldsInfo.fieldMap[responseName]
if !ok {
continue
}
// This compares every field in the list to every other field in this list
// (except to itself). If the list only has one item, nothing needs to
// be compared.
if len(fields) <= 1 {
continue
}
for i := 0; i < len(fields); i++ {
for k := i + 1; k < len(fields); k++ {
// within one collection is never mutually exclusive
isMutuallyExclusive := false
conflict := rule.findConflict(isMutuallyExclusive, responseName, fields[i], fields[k])
if conflict != nil {
conflicts = append(conflicts, *conflict)
}
}
}
}
return conflicts
}
// Collect all Conflicts between two collections of fields. This is similar to,
// but different from the `collectConflictsWithin` function above. This check
// assumes that `collectConflictsWithin` has already been called on each
// provided collection of fields. This is true because this validator traverses
// each individual selection set.
func (rule *overlappingFieldsCanBeMergedRule) collectConflictsBetween(conflicts []conflict, parentFieldsAreMutuallyExclusive bool,
fieldsInfo1 *fieldsAndFragmentNames,
fieldsInfo2 *fieldsAndFragmentNames) []conflict {
// A field map is a keyed collection, where each key represents a response
// name and the value at that key is a list of all fields which provide that
// response name. For any response name which appears in both provided field
// maps, each field from the first field map must be compared to every field
// in the second field map to find potential conflicts.
for _, responseName := range fieldsInfo1.fieldsOrder {
fields1, ok1 := fieldsInfo1.fieldMap[responseName]
fields2, ok2 := fieldsInfo2.fieldMap[responseName]
if !ok1 || !ok2 {
continue
}
for i := 0; i < len(fields1); i++ {
for k := 0; k < len(fields2); k++ {
conflict := rule.findConflict(parentFieldsAreMutuallyExclusive, responseName, fields1[i], fields2[k])
if conflict != nil {
conflicts = append(conflicts, *conflict)
}
}
}
}
return conflicts
}
// findConflict Determines if there is a conflict between two particular fields.
func (rule *overlappingFieldsCanBeMergedRule) findConflict(parentFieldsAreMutuallyExclusive bool, responseName string, field *fieldDefPair, field2 *fieldDefPair) *conflict {
parentType1 := field.ParentType
ast1 := field.Field
def1 := field.FieldDef
parentType2 := field2.ParentType
ast2 := field2.Field
def2 := field2.FieldDef
// If it is known that two fields could not possibly apply at the same
// time, due to the parent types, then it is safe to permit them to diverge
// in aliased field or arguments used as they will not present any ambiguity
// by differing.
// It is known that two parent types could never overlap if they are
// different Object types. Interface or Union types might overlap - if not
// in the current state of the schema, then perhaps in some future version,
// thus may not safely diverge.
_, isParentType1Object := parentType1.(*Object)
_, isParentType2Object := parentType2.(*Object)
areMutuallyExclusive := parentFieldsAreMutuallyExclusive || parentType1 != parentType2 && isParentType1Object && isParentType2Object
// The return type for each field.
var type1 Type
var type2 Type
if def1 != nil {
type1 = def1.Type
}
if def2 != nil {
type2 = def2.Type
}
if !areMutuallyExclusive {
// Two aliases must refer to the same field.
name1 := ""
name2 := ""
if ast1.Name != nil {
name1 = ast1.Name.Value
}
if ast2.Name != nil {
name2 = ast2.Name.Value
}
if name1 != name2 {
return &conflict{
Reason: conflictReason{
Name: responseName,
Message: fmt.Sprintf(`%v and %v are different fields`, name1, name2),
},
FieldsLeft: []ast.Node{ast1},
FieldsRight: []ast.Node{ast2},
}
}
// Two field calls must have the same arguments.
if !sameArguments(ast1.Arguments, ast2.Arguments) {
return &conflict{
Reason: conflictReason{
Name: responseName,
Message: `they have differing arguments`,
},
FieldsLeft: []ast.Node{ast1},
FieldsRight: []ast.Node{ast2},
}
}
}
if type1 != nil && type2 != nil && doTypesConflict(type1, type2) {
return &conflict{
Reason: conflictReason{
Name: responseName,
Message: fmt.Sprintf(`they return conflicting types %v and %v`, type1, type2),
},
FieldsLeft: []ast.Node{ast1},
FieldsRight: []ast.Node{ast2},
}
}
// Collect and compare sub-fields. Use the same "visited fragment names" list
// for both collections so fields in a fragment reference are never
// compared to themselves.
selectionSet1 := ast1.SelectionSet
selectionSet2 := ast2.SelectionSet
if selectionSet1 != nil && selectionSet2 != nil {
conflicts := rule.findConflictsBetweenSubSelectionSets(areMutuallyExclusive, GetNamed(type1), selectionSet1, GetNamed(type2), selectionSet2)
return subfieldConflicts(conflicts, responseName, ast1, ast2)
}
return nil
}
// Given a selection set, return the collection of fields (a mapping of response
// name to field ASTs and definitions) as well as a list of fragment names
// referenced via fragment spreads.
func (rule *overlappingFieldsCanBeMergedRule) getFieldsAndFragmentNames(parentType Named, selectionSet *ast.SelectionSet) *fieldsAndFragmentNames {
if cached, ok := rule.cacheMap[selectionSet]; ok && cached != nil {
return cached
}
astAndDefs := astAndDefCollection{}
fieldsOrder := []string{}
fragmentNames := []string{}
fragmentNamesMap := map[string]bool{}
var collectFieldsAndFragmentNames func(parentType Named, selectionSet *ast.SelectionSet)
collectFieldsAndFragmentNames = func(parentType Named, selectionSet *ast.SelectionSet) {
for _, selection := range selectionSet.Selections {
switch selection := selection.(type) {
case *ast.Field:
fieldName := ""
if selection.Name != nil {
fieldName = selection.Name.Value
}
var fieldDef *FieldDefinition
if parentType, ok := parentType.(*Object); ok && parentType != nil {
fieldDef, _ = parentType.Fields()[fieldName]
}
if parentType, ok := parentType.(*Interface); ok && parentType != nil {
fieldDef, _ = parentType.Fields()[fieldName]
}
responseName := fieldName
if selection.Alias != nil {
responseName = selection.Alias.Value
}
fieldDefPairs, ok := astAndDefs[responseName]
if !ok || fieldDefPairs == nil {
fieldDefPairs = []*fieldDefPair{}
fieldsOrder = append(fieldsOrder, responseName)
}
fieldDefPairs = append(fieldDefPairs, &fieldDefPair{
ParentType: parentType,
Field: selection,
FieldDef: fieldDef,
})
astAndDefs[responseName] = fieldDefPairs
case *ast.FragmentSpread:
fieldName := ""
if selection.Name != nil {
fieldName = selection.Name.Value
}
if val, ok := fragmentNamesMap[fieldName]; !ok || !val {
fragmentNamesMap[fieldName] = true
fragmentNames = append(fragmentNames, fieldName)
}
case *ast.InlineFragment:
typeCondition := selection.TypeCondition
inlineFragmentType := parentType
if typeCondition != nil {
ttype, err := typeFromAST(*(rule.context.Schema()), typeCondition)
if err == nil {
inlineFragmentType, _ = ttype.(Named)
}
}
collectFieldsAndFragmentNames(inlineFragmentType, selection.SelectionSet)
}
}
}
collectFieldsAndFragmentNames(parentType, selectionSet)
cached := &fieldsAndFragmentNames{
fieldMap: astAndDefs,
fieldsOrder: fieldsOrder,
fragmentNames: fragmentNames,
}
rule.cacheMap[selectionSet] = cached
return cached
}
func (rule *overlappingFieldsCanBeMergedRule) getReferencedFieldsAndFragmentNames(fragment *ast.FragmentDefinition) *fieldsAndFragmentNames {
// Short-circuit building a type from the AST if possible.
if cached, ok := rule.cacheMap[fragment.SelectionSet]; ok && cached != nil {
return cached
}
fragmentType, err := typeFromAST(*(rule.context.Schema()), fragment.TypeCondition)
if err != nil {
return nil
}
return rule.getFieldsAndFragmentNames(fragmentType, fragment.SelectionSet)
}
type conflictReason struct {
Name string
Message interface{} // conflictReason || []conflictReason
}
type conflict struct {
Reason conflictReason
FieldsLeft []ast.Node
FieldsRight []ast.Node
}
// a.k.a AstAndDef
type fieldDefPair struct {
ParentType Named
Field *ast.Field
FieldDef *FieldDefinition
}
type astAndDefCollection map[string][]*fieldDefPair
// cache struct for fields, its order and fragments names
type fieldsAndFragmentNames struct {
fieldMap astAndDefCollection
fieldsOrder []string // stores the order of field names in fieldMap
fragmentNames []string
}
// pairSet A way to keep track of pairs of things when the ordering of the pair does
// not matter. We do this by maintaining a sort of double adjacency sets.
type pairSet struct {
data map[string]map[string]bool
}
func newPairSet() *pairSet {
return &pairSet{
data: map[string]map[string]bool{},
}
}
func (pair *pairSet) Has(a string, b string, areMutuallyExclusive bool) bool {
first, ok := pair.data[a]
if !ok || first == nil {
return false
}
res, ok := first[b]
if !ok {
return false
}
// areMutuallyExclusive being false is a superset of being true,
// hence if we want to know if this PairSet "has" these two with no
// exclusivity, we have to ensure it was added as such.
if !areMutuallyExclusive {
return res == false
}
return true
}
func (pair *pairSet) Add(a string, b string, areMutuallyExclusive bool) {
pair.data = pairSetAdd(pair.data, a, b, areMutuallyExclusive)
pair.data = pairSetAdd(pair.data, b, a, areMutuallyExclusive)
}
func pairSetAdd(data map[string]map[string]bool, a, b string, areMutuallyExclusive bool) map[string]map[string]bool {
set, ok := data[a]
if !ok || set == nil {
set = map[string]bool{}
}
set[b] = areMutuallyExclusive
data[a] = set
return data
}
func sameArguments(args1 []*ast.Argument, args2 []*ast.Argument) bool {
if len(args1) != len(args2) {
return false
}
for _, arg1 := range args1 {
arg1Name := ""
if arg1.Name != nil {
arg1Name = arg1.Name.Value
}
var foundArgs2 *ast.Argument
for _, arg2 := range args2 {
arg2Name := ""
if arg2.Name != nil {
arg2Name = arg2.Name.Value
}
if arg1Name == arg2Name {
foundArgs2 = arg2
}
break
}
if foundArgs2 == nil {
return false
}
if sameValue(arg1.Value, foundArgs2.Value) == false {
return false
}
}
return true
}
func sameValue(value1 ast.Value, value2 ast.Value) bool {
if value1 == nil && value2 == nil {
return true
}
val1 := printer.Print(value1)
val2 := printer.Print(value2)
return val1 == val2
}
// Two types conflict if both types could not apply to a value simultaneously.
// Composite types are ignored as their individual field types will be compared
// later recursively. However List and Non-Null types must match.
func doTypesConflict(type1 Output, type2 Output) bool {
if type1, ok := type1.(*List); ok {
if type2, ok := type2.(*List); ok {
return doTypesConflict(type1.OfType, type2.OfType)
}
return true
}
if type2, ok := type2.(*List); ok {
if type1, ok := type1.(*List); ok {
return doTypesConflict(type1.OfType, type2.OfType)
}
return true
}
if type1, ok := type1.(*NonNull); ok {
if type2, ok := type2.(*NonNull); ok {
return doTypesConflict(type1.OfType, type2.OfType)
}
return true
}
if type2, ok := type2.(*NonNull); ok {
if type1, ok := type1.(*NonNull); ok {
return doTypesConflict(type1.OfType, type2.OfType)
}
return true
}
if IsLeafType(type1) || IsLeafType(type2) {
return type1 != type2
}
return false
}
// subfieldConflicts Given a series of Conflicts which occurred between two sub-fields, generate a single Conflict.
func subfieldConflicts(conflicts []conflict, responseName string, ast1 *ast.Field, ast2 *ast.Field) *conflict {
if len(conflicts) > 0 {
conflictReasons := []conflictReason{}
conflictFieldsLeft := []ast.Node{ast1}
conflictFieldsRight := []ast.Node{ast2}
for _, c := range conflicts {
conflictReasons = append(conflictReasons, c.Reason)
conflictFieldsLeft = append(conflictFieldsLeft, c.FieldsLeft...)
conflictFieldsRight = append(conflictFieldsRight, c.FieldsRight...)
}
return &conflict{
Reason: conflictReason{
Name: responseName,
Message: conflictReasons,
},
FieldsLeft: conflictFieldsLeft,
FieldsRight: conflictFieldsRight,
}
}
return nil
}
You can’t perform that action at this time.