Repository: jensneuse/graphql-go-tools
Pull request #336 (2022-02-05 00:33:36)

Implement copying of query document AST nodes
This PR implements copying of AST nodes used in query documents. Copying
will be used when normalizing operations for federation. Selection sets
will be copied when "expanding" inline fragments and inserting inline
fragments for fields with interface types. See the plan in #328 for
details.

I'm not sure everything that's being copied needs to be copied. For
example, it seems like it would be odd for someone to modify a value,
but I copy everything except byte slice references to be one the safe
side.

Copying could also be utilized when inlining fragments, if we wanted.
Right now there's a big warning on ReplaceFragmentSpread about field
aliasing (and it isn't clear to be that field de-duplication fully fixes
the aliasing).


Patch:
diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go
index 1205f2ff..bc11764f 100644
--- a/pkg/ast/ast.go
+++ b/pkg/ast/ast.go
@@ -161,6 +161,10 @@ func (d *Document) NextRefIndex() int {
 	return d.RefIndex
 }
 
+func (d *Document) NewEmptyRefs() []int {
+	return d.Refs[d.NextRefIndex()][:0]
+}
+
 func (d *Document) AddRootNode(node Node) {
 	d.RootNodes = append(d.RootNodes, node)
 	d.Index.AddNodeStr(d.NodeNameUnsafeString(node), node)
diff --git a/pkg/ast/ast_argument.go b/pkg/ast/ast_argument.go
index 94afb38e..b896a86e 100644
--- a/pkg/ast/ast_argument.go
+++ b/pkg/ast/ast_argument.go
@@ -21,6 +21,24 @@ type Argument struct {
 	Value Value              // e.g. 100 or "Bar"
 }
 
+func (d *Document) CopyArgument(ref int) int {
+	return d.AddArgument(Argument{
+		Name: d.Arguments[ref].Name, // Doesn't need to be copied.
+		Value: Value{
+			Kind: d.Arguments[ref].Value.Kind,
+			Ref:  d.copyValueRef(d.Arguments[ref].Value.Kind, d.Arguments[ref].Value.Ref),
+		},
+	})
+}
+
+func (d *Document) CopyArgumentList(list ArgumentList) ArgumentList {
+	refs := d.NewEmptyRefs()
+	for _, r := range list.Refs {
+		refs = append(refs, d.CopyArgument(r))
+	}
+	return ArgumentList{Refs: refs}
+}
+
 func (d *Document) PrintArgument(ref int, w io.Writer) error {
 	_, err := w.Write(d.Input.ByteSlice(d.Arguments[ref].Name))
 	if err != nil {
diff --git a/pkg/ast/ast_directive.go b/pkg/ast/ast_directive.go
index ea1c35bf..68d949a5 100644
--- a/pkg/ast/ast_directive.go
+++ b/pkg/ast/ast_directive.go
@@ -41,6 +41,26 @@ func (l *DirectiveList) RemoveDirectiveByName(document *Document, name string) {
 	}
 }
 
+func (d *Document) CopyDirective(ref int) int {
+	var arguments ArgumentList
+	if d.Directives[ref].HasArguments {
+		arguments = d.CopyArgumentList(d.Directives[ref].Arguments)
+	}
+	return d.AddDirective(Directive{
+		Name:         d.Directives[ref].Name, // Doesn't need to be copied.
+		HasArguments: d.Directives[ref].HasArguments,
+		Arguments:    arguments,
+	})
+}
+
+func (d *Document) CopyDirectiveList(list DirectiveList) DirectiveList {
+	refs := d.NewEmptyRefs()
+	for _, r := range list.Refs {
+		refs = append(refs, d.CopyDirective(r))
+	}
+	return DirectiveList{Refs: refs}
+}
+
 func (d *Document) PrintDirective(ref int, w io.Writer) error {
 	_, err := w.Write(literal.AT)
 	if err != nil {
diff --git a/pkg/ast/ast_field.go b/pkg/ast/ast_field.go
index b5117132..54fe0676 100644
--- a/pkg/ast/ast_field.go
+++ b/pkg/ast/ast_field.go
@@ -19,6 +19,31 @@ type Field struct {
 	Position      position.Position
 }
 
+func (d *Document) CopyField(ref int) int {
+	var arguments ArgumentList
+	var directives DirectiveList
+	var selectionSet int
+	if d.Fields[ref].HasArguments {
+		arguments = d.CopyArgumentList(d.Fields[ref].Arguments)
+	}
+	if d.Fields[ref].HasDirectives {
+		directives = d.CopyDirectiveList(d.Fields[ref].Directives)
+	}
+	if d.Fields[ref].HasSelections {
+		selectionSet = d.CopySelectionSet(d.Fields[ref].SelectionSet)
+	}
+	return d.AddField(Field{
+		Name:          d.Fields[ref].Name, // Doesn't need to be copied.
+		Alias:         d.CopyAlias(d.Fields[ref].Alias),
+		HasArguments:  d.Fields[ref].HasArguments,
+		Arguments:     arguments,
+		HasDirectives: d.Fields[ref].HasDirectives,
+		Directives:    directives,
+		HasSelections: d.Fields[ref].HasSelections,
+		SelectionSet:  selectionSet,
+	}).Ref
+}
+
 func (d *Document) FieldNameBytes(ref int) ByteSlice {
 	return d.Input.ByteSlice(d.Fields[ref].Name)
 }
@@ -67,7 +92,6 @@ func (d *Document) FieldDirectives(ref int) []int {
 }
 
 func (d *Document) FieldsHaveSameShape(left, right int) bool {
-
 	leftAliasDefined := d.FieldAliasIsDefined(left)
 	rightAliasDefined := d.FieldAliasIsDefined(right)
 
diff --git a/pkg/ast/ast_field_alias.go b/pkg/ast/ast_field_alias.go
index 3cbb5db6..ffb27f48 100644
--- a/pkg/ast/ast_field_alias.go
+++ b/pkg/ast/ast_field_alias.go
@@ -11,6 +11,13 @@ type Alias struct {
 	Colon     position.Position  // :
 }
 
+func (d *Document) CopyAlias(alias Alias) Alias {
+	return Alias{
+		IsDefined: alias.IsDefined,
+		Name:      alias.Name, // Doesn't need to be copied.
+	}
+}
+
 func (d *Document) FieldAliasOrNameBytes(ref int) ByteSlice {
 	if d.FieldAliasIsDefined(ref) {
 		return d.FieldAliasBytes(ref)
diff --git a/pkg/ast/ast_fragment_spread.go b/pkg/ast/ast_fragment_spread.go
index f09c091a..5fa291ac 100644
--- a/pkg/ast/ast_fragment_spread.go
+++ b/pkg/ast/ast_fragment_spread.go
@@ -15,6 +15,23 @@ type FragmentSpread struct {
 	Directives    DirectiveList // optional, e.g. @foo
 }
 
+func (d *Document) CopyFragmentSpread(ref int) int {
+	var directives DirectiveList
+	if d.FragmentSpreads[ref].HasDirectives {
+		directives = d.CopyDirectiveList(d.FragmentSpreads[ref].Directives)
+	}
+	return d.AddFragmentSpread(FragmentSpread{
+		FragmentName:  d.FragmentSpreads[ref].FragmentName, // Doesn't need to be copied.
+		HasDirectives: d.FragmentSpreads[ref].HasDirectives,
+		Directives:    directives,
+	})
+}
+
+func (d *Document) AddFragmentSpread(spread FragmentSpread) int {
+	d.FragmentSpreads = append(d.FragmentSpreads, spread)
+	return len(d.FragmentSpreads) - 1
+}
+
 func (d *Document) FragmentSpreadNameBytes(ref int) ByteSlice {
 	return d.Input.ByteSlice(d.FragmentSpreads[ref].FragmentName)
 }
diff --git a/pkg/ast/ast_inline_fragment.go b/pkg/ast/ast_inline_fragment.go
index ddbcaa5f..f5d21451 100644
--- a/pkg/ast/ast_inline_fragment.go
+++ b/pkg/ast/ast_inline_fragment.go
@@ -21,6 +21,24 @@ type InlineFragment struct {
 	HasSelections bool
 }
 
+func (d *Document) CopyInlineFragment(ref int) int {
+	var directives DirectiveList
+	var selectionSet int
+	if d.InlineFragments[ref].HasDirectives {
+		directives = d.CopyDirectiveList(d.InlineFragments[ref].Directives)
+	}
+	if d.InlineFragments[ref].HasSelections {
+		selectionSet = d.CopySelectionSet(d.InlineFragments[ref].SelectionSet)
+	}
+	return d.AddInlineFragment(InlineFragment{
+		TypeCondition: d.InlineFragments[ref].TypeCondition, // Value type; doesn't need to be copied.
+		HasDirectives: d.InlineFragments[ref].HasDirectives,
+		Directives:    directives,
+		SelectionSet:  selectionSet,
+		HasSelections: d.InlineFragments[ref].HasSelections,
+	})
+}
+
 func (d *Document) InlineFragmentTypeConditionName(ref int) ByteSlice {
 	if d.InlineFragments[ref].TypeCondition.Type == -1 {
 		return nil
diff --git a/pkg/ast/ast_object_field.go b/pkg/ast/ast_object_field.go
index b6013765..1a081f83 100644
--- a/pkg/ast/ast_object_field.go
+++ b/pkg/ast/ast_object_field.go
@@ -16,6 +16,16 @@ type ObjectField struct {
 	Value Value              // e.g. 12.43
 }
 
+func (d *Document) CopyObjectField(ref int) int {
+	return d.AddObjectField(ObjectField{
+		Name: d.ObjectFields[ref].Name, // Doesn't need to be copied
+		Value: Value{
+			Kind: d.ObjectFields[ref].Value.Kind,
+			Ref:  d.copyValueRef(d.ObjectFields[ref].Value.Kind, d.ObjectFields[ref].Value.Ref),
+		},
+	})
+}
+
 func (d *Document) ObjectField(ref int) ObjectField {
 	return d.ObjectFields[ref]
 }
diff --git a/pkg/ast/ast_selection.go b/pkg/ast/ast_selection.go
index 04843b21..d14b0d33 100644
--- a/pkg/ast/ast_selection.go
+++ b/pkg/ast/ast_selection.go
@@ -28,6 +28,34 @@ type Selection struct {
 	Ref  int           // reference to the actual selection
 }
 
+func (d *Document) CopySelection(ref int) int {
+	innerRef := -1
+
+	switch d.Selections[ref].Kind {
+	case SelectionKindField:
+		innerRef = d.CopyField(d.Selections[ref].Ref)
+	case SelectionKindFragmentSpread:
+		innerRef = d.CopyFragmentSpread(d.Selections[ref].Ref)
+	case SelectionKindInlineFragment:
+		innerRef = d.CopyInlineFragment(d.Selections[ref].Ref)
+	}
+
+	return d.AddSelectionToDocument(Selection{
+		Kind: d.Selections[ref].Kind,
+		Ref:  innerRef,
+	})
+}
+
+func (d *Document) CopySelectionSet(ref int) int {
+	refs := d.NewEmptyRefs()
+	for _, r := range d.SelectionSets[ref].SelectionRefs {
+		refs = append(refs, d.CopySelection(r))
+	}
+	return d.AddSelectionSetToDocument(SelectionSet{
+		SelectionRefs: refs,
+	})
+}
+
 func (d *Document) PrintSelections(selections []int) (out string) {
 	out += "["
 	for i, ref := range selections {
@@ -88,17 +116,27 @@ func (d *Document) SelectionsAfterFragmentSpread(fragmentSpread int, selectionSe
 	return d.SelectionsAfter(SelectionKindFragmentSpread, fragmentSpread, selectionSet)
 }
 
+func (d *Document) AddSelectionSetToDocument(set SelectionSet) int {
+	d.SelectionSets = append(d.SelectionSets, set)
+	return len(d.SelectionSets) - 1
+}
+
 func (d *Document) AddSelectionSet() Node {
-	d.SelectionSets = append(d.SelectionSets, SelectionSet{SelectionRefs: d.Refs[d.NextRefIndex()][:0]})
 	return Node{
 		Kind: NodeKindSelectionSet,
-		Ref:  len(d.SelectionSets) - 1,
+		Ref: d.AddSelectionSetToDocument(SelectionSet{
+			SelectionRefs: d.Refs[d.NextRefIndex()][:0],
+		}),
 	}
 }
 
-func (d *Document) AddSelection(set int, selection Selection) {
+func (d *Document) AddSelectionToDocument(selection Selection) int {
 	d.Selections = append(d.Selections, selection)
-	d.SelectionSets[set].SelectionRefs = append(d.SelectionSets[set].SelectionRefs, len(d.Selections)-1)
+	return len(d.Selections) - 1
+}
+
+func (d *Document) AddSelection(set int, selection Selection) {
+	d.SelectionSets[set].SelectionRefs = append(d.SelectionSets[set].SelectionRefs, d.AddSelectionToDocument(selection))
 }
 
 func (d *Document) EmptySelectionSet(ref int) {
diff --git a/pkg/ast/ast_val_enum_value.go b/pkg/ast/ast_val_enum_value.go
index 8940a4fa..4020283d 100644
--- a/pkg/ast/ast_val_enum_value.go
+++ b/pkg/ast/ast_val_enum_value.go
@@ -1,6 +1,8 @@
 package ast
 
-import "github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
+import (
+	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
+)
 
 // EnumValue
 // example:
@@ -9,6 +11,12 @@ type EnumValue struct {
 	Name ByteSliceReference // e.g. ORIGIN
 }
 
+func (d *Document) CopyEnumValue(ref int) int {
+	return d.AddEnumValue(EnumValue{
+		Name: d.EnumValues[ref].Name, // Doesn't need to be copied.
+	})
+}
+
 func (d *Document) EnumValueName(ref int) ByteSliceReference {
 	return d.EnumValues[ref].Name
 }
diff --git a/pkg/ast/ast_val_float_value.go b/pkg/ast/ast_val_float_value.go
index 4c50f3d6..c0d0af96 100644
--- a/pkg/ast/ast_val_float_value.go
+++ b/pkg/ast/ast_val_float_value.go
@@ -16,6 +16,13 @@ type FloatValue struct {
 	Raw          ByteSliceReference // e.g. 13.37
 }
 
+func (d *Document) CopyFloatValue(ref int) int {
+	return d.AddFloatValue(FloatValue{
+		Negative: d.FloatValues[ref].Negative,
+		Raw:      d.FloatValues[ref].Raw, // Doesn't need to be copied.
+	})
+}
+
 func (d *Document) FloatValueAsFloat32(ref int) (out float32) {
 	in := d.Input.ByteSlice(d.FloatValues[ref].Raw)
 	out = unsafebytes.BytesToFloat32(in)
diff --git a/pkg/ast/ast_val_int_value.go b/pkg/ast/ast_val_int_value.go
index 6cf9e90b..a3e8ac70 100644
--- a/pkg/ast/ast_val_int_value.go
+++ b/pkg/ast/ast_val_int_value.go
@@ -16,6 +16,13 @@ type IntValue struct {
 	Raw          ByteSliceReference // e.g. 123
 }
 
+func (d *Document) CopyIntValue(ref int) int {
+	return d.AddIntValue(IntValue{
+		Negative: d.IntValues[ref].Negative,
+		Raw:      d.IntValues[ref].Raw, // Doesn't need to be copied.
+	})
+}
+
 func (d *Document) IntValueAsInt(ref int) (out int64) {
 	in := d.Input.ByteSlice(d.IntValues[ref].Raw)
 	out = unsafebytes.BytesToInt64(in)
diff --git a/pkg/ast/ast_val_list_value.go b/pkg/ast/ast_val_list_value.go
index 4d2aaba0..94218edd 100644
--- a/pkg/ast/ast_val_list_value.go
+++ b/pkg/ast/ast_val_list_value.go
@@ -8,6 +8,16 @@ type ListValue struct {
 	RBRACK position.Position // ]
 }
 
+func (d *Document) CopyListValue(ref int) int {
+	refs := d.NewEmptyRefs()
+	for _, r := range d.ListValues[ref].Refs {
+		refs = append(refs, d.CopyValue(r))
+	}
+	return d.AddListValue(ListValue{
+		Refs: refs,
+	})
+}
+
 func (d *Document) ListValuesAreEqual(left, right int) bool {
 	leftValues, rightValues := d.ListValues[left].Refs, d.ListValues[right].Refs
 	if len(leftValues) != len(rightValues) {
diff --git a/pkg/ast/ast_val_object_value.go b/pkg/ast/ast_val_object_value.go
index 2ddfb666..4bfbd388 100644
--- a/pkg/ast/ast_val_object_value.go
+++ b/pkg/ast/ast_val_object_value.go
@@ -11,6 +11,16 @@ type ObjectValue struct {
 	RBRACE position.Position
 }
 
+func (d *Document) CopyObjectValue(ref int) int {
+	refs := d.NewEmptyRefs()
+	for _, r := range d.ObjectValues[ref].Refs {
+		refs = append(refs, d.CopyObjectField(r))
+	}
+	return d.AddObjectValue(ObjectValue{
+		Refs: refs,
+	})
+}
+
 func (d *Document) AddObjectValue(value ObjectValue) (ref int) {
 	d.ObjectValues = append(d.ObjectValues, value)
 	return len(d.ObjectValues) - 1
diff --git a/pkg/ast/ast_val_string_value.go b/pkg/ast/ast_val_string_value.go
index 88b5a525..c7f7ba65 100644
--- a/pkg/ast/ast_val_string_value.go
+++ b/pkg/ast/ast_val_string_value.go
@@ -14,6 +14,13 @@ type StringValue struct {
 	Content     ByteSliceReference // e.g. foo
 }
 
+func (d *Document) CopyStringValue(ref int) int {
+	return d.AddStringValue(StringValue{
+		BlockString: d.StringValues[ref].BlockString,
+		Content:     d.StringValues[ref].Content, // Doesn't need to be copied.
+	})
+}
+
 func (d *Document) StringValue(ref int) StringValue {
 	return d.StringValues[ref]
 }
diff --git a/pkg/ast/ast_val_variable_value.go b/pkg/ast/ast_val_variable_value.go
index 4b2477a3..8e1fff47 100644
--- a/pkg/ast/ast_val_variable_value.go
+++ b/pkg/ast/ast_val_variable_value.go
@@ -15,6 +15,12 @@ type VariableValue struct {
 	Name   ByteSliceReference // e.g. devicePicSize
 }
 
+func (d *Document) CopyVariableValue(ref int) int {
+	return d.AddVariableValue(VariableValue{
+		Name: d.VariableValues[ref].Name, // Doesn't need to be copied.
+	})
+}
+
 func (d *Document) VariableValueNameBytes(ref int) ByteSlice {
 	return d.Input.ByteSlice(d.VariableValues[ref].Name)
 }
diff --git a/pkg/ast/ast_value.go b/pkg/ast/ast_value.go
index 901a29c5..972d6564 100644
--- a/pkg/ast/ast_value.go
+++ b/pkg/ast/ast_value.go
@@ -32,6 +32,40 @@ type Value struct {
 	Ref  int
 }
 
+func (d *Document) CopyValue(ref int) int {
+	return d.AddValue(Value{
+		Kind: d.Values[ref].Kind,
+		Ref:  d.copyValueRef(d.Values[ref].Kind, d.Values[ref].Ref),
+	})
+}
+
+func (d *Document) copyValueRef(kind ValueKind, valueRef int) int {
+	switch kind {
+	case ValueKindString:
+		return d.CopyStringValue(valueRef)
+	case ValueKindBoolean:
+		// Nothing to copy!
+		return valueRef
+	case ValueKindInteger:
+		return d.CopyIntValue(valueRef)
+	case ValueKindFloat:
+		return d.CopyFloatValue(valueRef)
+	case ValueKindVariable:
+		return d.CopyVariableValue(valueRef)
+	case ValueKindNull:
+		// Nothing to copy!
+		return -1
+	case ValueKindList:
+		return d.CopyListValue(valueRef)
+	case ValueKindObject:
+		return d.CopyObjectValue(valueRef)
+	case ValueKindEnum:
+		return d.CopyEnumValue(valueRef)
+	default:
+		return -1
+	}
+}
+
 func (d *Document) ValueContentBytes(value Value) ByteSlice {
 	switch value.Kind {
 	case ValueKindEnum:
@@ -275,4 +309,4 @@ func (d *Document) ValuesAreEqual(left, right Value) bool {
 func (d *Document) AddValue(value Value) (ref int) {
 	d.Values = append(d.Values, value)
 	return len(d.Values) - 1
-}
\ No newline at end of file
+}


Test patch:
diff --git a/pkg/ast/ast_test.go b/pkg/ast/ast_test.go
index 7c61c5be..0a4e78be 100644
--- a/pkg/ast/ast_test.go
+++ b/pkg/ast/ast_test.go
@@ -14,7 +14,6 @@ import (
 // Create a new document with initialized slices.
 // In case you're on a hot path you always want to use a pre-initialized Document.
 func ExampleNewDocument() {
-
 	schema := []byte(`
 		schema {
 			query: Query
@@ -34,7 +33,6 @@ func ExampleNewDocument() {
 // Create a new Document without pre-initializing slices.
 // Use this if you want to manually create a new Document
 func ExampleDocument() {
-
 	// create the same doc as in NewDocument() example but manually.
 
 	doc := &ast.Document{}
@@ -135,6 +133,120 @@ func ExampleDocument() {
 	doc.RootNodes = append(doc.RootNodes, ast.Node{Kind: ast.NodeKindObjectTypeDefinition, Ref: queryTypeRef})
 }
 
+func TestCopying(t *testing.T) {
+	doc, report := astparser.ParseGraphqlDocumentString(`
+		query testQuery($someVariable: String!) {
+			user {
+				fieldToCopy {
+					booleanArgField(arg: true)
+					enumArgField(arg: SOME_ENUM_VALUE)
+					floatArgField(arg: 3.14)
+					intArgField(arg: 6)
+					listArgField(arg: [1, 2, 3, 4])
+					objectArgField(arg: {key: "value"})
+					stringArgField(arg: "hello")
+					variableArgField(arg: $someVariable)
+					twoArgField(argOne: true, argTwo: false)
+					scalarField
+					objectField {
+						fieldOne
+						fieldTwo
+					}
+					aliasedField: nonAliasedField
+					...namedFragment
+					... on SomeType {
+						inlineFragmentField
+						... on AnotherType {
+							nestedInlineFragmentField
+						}
+					}
+					directiveField @requires(fields: "scalarField") @anotherDirective()
+				}
+			}
+		}
+
+		fragment namedFragment on SomeType {
+			fragmentField
+		}
+	`)
+
+	assert.False(t, report.HasErrors())
+
+	for ref := range doc.Fields {
+		if doc.FieldNameString(ref) == "user" {
+			selectionSet := doc.Fields[ref].SelectionSet
+			selectionToCopy := doc.SelectionSets[selectionSet].SelectionRefs[0]
+			doc.AddSelection(selectionSet, doc.Selections[doc.CopySelection(selectionToCopy)])
+			break
+		}
+	}
+
+	out, err := astprinter.PrintStringIndent(&doc, nil, "  ")
+
+	assert.NoError(t, err)
+
+	expected := `query testQuery($someVariable: String!){
+    user {
+        fieldToCopy {
+            booleanArgField(arg: true)
+            enumArgField(arg: SOME_ENUM_VALUE)
+            floatArgField(arg: 3.14)
+            intArgField(arg: 6)
+            listArgField(arg: [1,2,3,4])
+            objectArgField(arg: {key: "value"})
+            stringArgField(arg: "hello")
+            variableArgField(arg: $someVariable)
+            twoArgField(argOne: true, argTwo: false)
+            scalarField
+            objectField {
+                fieldOne
+                fieldTwo
+            }
+            aliasedField: nonAliasedField
+            ...namedFragment
+            ... on SomeType {
+                inlineFragmentField
+                ... on AnotherType {
+                    nestedInlineFragmentField
+                }
+            }
+            directiveField @requires(fields: "scalarField") @anotherDirective
+        }
+        fieldToCopy {
+            booleanArgField(arg: true)
+            enumArgField(arg: SOME_ENUM_VALUE)
+            floatArgField(arg: 3.14)
+            intArgField(arg: 6)
+            listArgField(arg: [1,2,3,4])
+            objectArgField(arg: {key: "value"})
+            stringArgField(arg: "hello")
+            variableArgField(arg: $someVariable)
+            twoArgField(argOne: true, argTwo: false)
+            scalarField
+            objectField {
+                fieldOne
+                fieldTwo
+            }
+            aliasedField: nonAliasedField
+            ...namedFragment
+            ... on SomeType {
+                inlineFragmentField
+                ... on AnotherType {
+                    nestedInlineFragmentField
+                }
+            }
+            directiveField @requires(fields: "scalarField") @anotherDirective
+        }
+    }
+}
+
+fragment namedFragment on SomeType {
+    fragmentField
+}`
+
+	assert.Equal(t, expected, out)
+}
+
 func TestKinds(t *testing.T) {
 	expectedArray := func(start, count int) (out []int) {
 		for i := start; i < start+count; i++ {