Repository: graphql-java-kickstart/graphql-java-tools (kotlin)

Issue:
Circular input property leads to stackoverflow
After updating graphql-java-tools to 6.1.0 i have encountered a stackoverflow exception on startup. I think that the problem was introduced in this commit: https://github.com/graphql-java-kickstart/graphql-java-tools/commit/10ee43f3e00f25194323a6ab7c1a9a4481726ca6 and happens because of the interaction between `determineInputType` and `createInputObject`. 
Example schema:

```
input A {
    id: ID!
    b: B
}

input B {
   id: ID!
   a: A
}
```

According to the spec this should be allowed (https://github.com/graphql/graphql-spec/pull/445, https://github.com/graphql/graphql-spec/issues/189), which leads me to believe that this is a bug.

Pull request:
Fixes #409 

## Checklist
- [x] Pull requests follows the [contribution guide](https://github.com/graphql-java-kickstart/graphql-java-tools/wiki/Contribution-guide)
- [x] New or modified functionality is covered by tests

## Description
I tried to fix #409 with this PR. Quick rundown of the problem and what my attempt to fix it was:
This commit https://github.com/graphql-java-kickstart/graphql-java-tools/commit/10ee43f3e00f25194323a6ab7c1a9a4481726ca6 tried to address https://github.com/graphql-java-kickstart/graphql-java-tools/issues/385 by using `GraphQLInputObjectTypes` instead of just `GraphQLTypeReferences`. The `GraphQLInputObjectTypes` will be populated with a full list of field definitions, which in turn will create `GraphQLInputObjectTypes` if necessary. The stackoverflow occured when a field needed to create a `GraphQLInputObjectType`, which then would have a reference back to any ancestor in the creation chain. Since at that point the ancestor did not yet exist in the `inputObjects` list the function tries to create it again, which restarts the whole chain.

My fix: Instead of using `GraphQLInputObjectTypes` for everything we keep a list of all ancestors (called referencingInputObjects). If we then encounter a type which already is in this list we return a `GraphQLTypeReference`  instead of the full `GraphQLInputObjectType` again. This should hopefully allow https://github.com/graphql-java-kickstart/graphql-java-tools/issues/385 to still be functional, while also fixing #409, although i did **not** test, for that. So maybe someone more knowledgeable should have a look at the specific use-case of #385

Patch:
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
index a1f6bb38..b53d6c24 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
@@ -76,7 +76,7 @@ class SchemaParser internal constructor(
         val inputObjects: MutableList<GraphQLInputObjectType> = mutableListOf()
         inputObjectDefinitions.forEach {
             if (inputObjects.none { io -> io.name == it.name }) {
-                inputObjects.add(createInputObject(it, inputObjects))
+                inputObjects.add(createInputObject(it, inputObjects, mutableSetOf()))
             }
         }
         val interfaces = interfaceDefinitions.map { createInterfaceObject(it, inputObjects) }
@@ -155,7 +155,8 @@ class SchemaParser internal constructor(
         return schemaGeneratorDirectiveHelper.onObject(objectType, directiveHelperParameters)
     }
 
-    private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLInputObjectType {
+    private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List<GraphQLInputObjectType>,
+                                  referencingInputObjects: MutableSet<String>): GraphQLInputObjectType {
         val extensionDefinitions = inputExtensionDefinitions.filter { it.name == definition.name }
 
         val builder = GraphQLInputObjectType.newInputObject()
@@ -166,6 +167,8 @@ class SchemaParser internal constructor(
 
         builder.withDirectives(*buildDirectives(definition.directives, Introspection.DirectiveLocation.INPUT_OBJECT))
 
+        referencingInputObjects.add(definition.name)
+
         (extensionDefinitions + definition).forEach {
             it.inputValueDefinitions.forEach { inputDefinition ->
                 val fieldBuilder = GraphQLInputObjectField.newInputObjectField()
@@ -173,7 +176,7 @@ class SchemaParser internal constructor(
                     .definition(inputDefinition)
                     .description(if (inputDefinition.description != null) inputDefinition.description.content else getDocumentation(inputDefinition))
                     .defaultValue(buildDefaultValue(inputDefinition.defaultValue))
-                    .type(determineInputType(inputDefinition.type, inputObjects))
+                    .type(determineInputType(inputDefinition.type, inputObjects, referencingInputObjects))
                     .withDirectives(*buildDirectives(inputDefinition.directives, Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION))
                 builder.field(fieldBuilder.build())
             }
@@ -280,7 +283,7 @@ class SchemaParser internal constructor(
                 .name(argumentDefinition.name)
                 .definition(argumentDefinition)
                 .description(if (argumentDefinition.description != null) argumentDefinition.description.content else getDocumentation(argumentDefinition))
-                .type(determineInputType(argumentDefinition.type, inputObjects))
+                .type(determineInputType(argumentDefinition.type, inputObjects, setOf()))
                 .apply { buildDefaultValue(argumentDefinition.defaultValue)?.let { defaultValue(it) } }
                 .withDirectives(*buildDirectives(argumentDefinition.directives, Introspection.DirectiveLocation.ARGUMENT_DEFINITION))
 
@@ -380,7 +383,7 @@ class SchemaParser internal constructor(
             is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
             is InputObjectTypeDefinition -> {
                 log.info("Create input object")
-                createInputObject(typeDefinition, inputObjects)
+                createInputObject(typeDefinition, inputObjects, mutableSetOf())
             }
             is TypeName -> {
                 val scalarType = customScalars[typeDefinition.name]
@@ -398,16 +401,19 @@ class SchemaParser internal constructor(
             else -> throw SchemaError("Unknown type: $typeDefinition")
         }
 
-    private fun determineInputType(typeDefinition: Type<*>, inputObjects: List<GraphQLInputObjectType>) =
-        determineInputType(GraphQLInputType::class, typeDefinition, permittedTypesForInputObject, inputObjects) as GraphQLInputType
+    private fun determineInputType(typeDefinition: Type<*>, inputObjects: List<GraphQLInputObjectType>, referencingInputObjects: Set<String>) =
+        determineInputType(GraphQLInputType::class, typeDefinition, permittedTypesForInputObject, inputObjects, referencingInputObjects) as GraphQLInputType
 
-    private fun <T : Any> determineInputType(expectedType: KClass<T>, typeDefinition: Type<*>, allowedTypeReferences: Set<String>, inputObjects: List<GraphQLInputObjectType>): GraphQLType =
+    private fun <T : Any> determineInputType(expectedType: KClass<T>,
+                                             typeDefinition: Type<*>, allowedTypeReferences: Set<String>,
+                                             inputObjects: List<GraphQLInputObjectType>,
+                                             referencingInputObjects: Set<String>): GraphQLType =
         when (typeDefinition) {
             is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
             is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
             is InputObjectTypeDefinition -> {
                 log.info("Create input object")
-                createInputObject(typeDefinition, inputObjects)
+                createInputObject(typeDefinition, inputObjects, referencingInputObjects as MutableSet<String>)
             }
             is TypeName -> {
                 val scalarType = customScalars[typeDefinition.name]
@@ -425,9 +431,14 @@ class SchemaParser internal constructor(
                     } else {
                         val filteredDefinitions = inputObjectDefinitions.filter { it.name == typeDefinition.name }
                         if (filteredDefinitions.isNotEmpty()) {
-                            val inputObject = createInputObject(filteredDefinitions[0], inputObjects)
-                            (inputObjects as MutableList).add(inputObject)
-                            inputObject
+                            val referencingInputObject = referencingInputObjects.find { it == typeDefinition.name }
+                            if (referencingInputObject != null) {
+                                GraphQLTypeReference(referencingInputObject)
+                            } else {
+                                val inputObject = createInputObject(filteredDefinitions[0], inputObjects, referencingInputObjects as MutableSet<String>)
+                                (inputObjects as MutableList).add(inputObject)
+                                inputObject
+                            }
                         } else {
                             // todo: handle enum type
                             GraphQLTypeReference(typeDefinition.name)


Test patch:
diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy
index d08df14c..a773e4b9 100644
--- a/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy
+++ b/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy
@@ -368,6 +368,50 @@ class SchemaParserSpec extends Specification {
         noExceptionThrown()
     }
 
+    def "allow circular relations in input objects"() {
+        when:
+        SchemaParser.newParser().schemaString('''\
+            input A {
+                id: ID!
+                b: B
+            }
+            input B {
+                id: ID!
+                a: A
+            }
+            input C {
+                id: ID!
+                c: C
+            }
+            type Query {}
+            type Mutation {
+                test(input: A!): Boolean
+                testC(input: C!): Boolean
+            }
+            '''.stripIndent())
+        .resolvers(new GraphQLMutationResolver() {
+            static class A {
+                String id;
+                B b;
+            }
+            static class B {
+                String id;
+                A a;
+            }
+            static class C {
+                String id;
+                C c;
+            }
+            boolean test(A a) { return true }
+            boolean testC(C c) { return true }
+        }, new GraphQLQueryResolver() {})
+        .build()
+        .makeExecutableSchema()
+
+        then:
+        noExceptionThrown()
+    }
+
     enum EnumType {
         TEST
     }