From 994512e38b80677d3909347793198fe66971d953 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Tue, 17 Feb 2026 04:21:07 -0800 Subject: [PATCH 1/2] check oneof inhabitability --- src/type/__tests__/validation-test.ts | 193 ++++++++++++++++++++++++++ src/type/validate.ts | 65 +++++++++ 2 files changed, 258 insertions(+) diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index c1ed85d7b6..7c7a24660c 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -2409,6 +2409,199 @@ describe('Type System: OneOf Input Object fields must be nullable', () => { }); }); +describe('Type System: OneOf Input Objects must be inhabitable', () => { + it('accepts a OneOf Input Object with a scalar field', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + a: String + b: Int + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts a OneOf Input Object with an enum field', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + enum Color { RED GREEN BLUE } + + input A @oneOf { + a: Color + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts a OneOf Input Object with a list field', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + a: [A] + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts a OneOf Input Object referencing a non-OneOf input object', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + a: RegularInput + } + + input RegularInput { + x: String + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts a OneOf Input Object with at least one escape field', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + b: B + escape: String + } + + input B @oneOf { + a: A + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts mutually referencing OneOf types where one has a scalar escape', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + b: B + } + + input B @oneOf { + a: A + escape: Int + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts a OneOf referencing a non-OneOf which references back', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + b: RegularInput + } + + input RegularInput { + back: A + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts a OneOf with multiple fields where one escapes through chained OneOf types', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + b: B + c: C + } + + input B @oneOf { + a: A + } + + input C @oneOf { + a: A + escape: String + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('rejects a closed subgraph of one OneOf type', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + self: A + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'OneOf Input Object A must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', + locations: [{ line: 6, column: 7 }], + }, + ]); + }); + + it('rejects a closed subgraph of multiple OneOf types', () => { + const schema = buildSchema(` + type Query { + test(arg: A): String + } + + input A @oneOf { + b: B + } + + input B @oneOf { + c: C + } + + input C @oneOf { + a: A + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'OneOf Input Object A must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', + locations: [{ line: 6, column: 7 }], + }, + { + message: + 'OneOf Input Object B must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', + locations: [{ line: 10, column: 7 }], + }, + { + message: + 'OneOf Input Object C must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', + locations: [{ line: 14, column: 7 }], + }, + ]); + }); +}); + describe('Objects must adhere to Interface they implement', () => { it('accepts an Object which implements an Interface', () => { const schema = buildSchema(` diff --git a/src/type/validate.ts b/src/type/validate.ts index ede99c8054..4406fac3b0 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -365,6 +365,8 @@ function validateTypes(context: SchemaValidationContext): void { createInputObjectNonNullCircularRefsValidator(context); const validateInputObjectDefaultValueCircularRefs = createInputObjectDefaultValueCircularRefsValidator(context); + const validateOneOfInputObjectInhabitability = + createOneOfInputObjectInhabitabilityValidator(context); const typeMap = context.schema.getTypeMap(); for (const type of Object.values(typeMap)) { // Ensure all provided types are in fact GraphQL type. @@ -409,6 +411,11 @@ function validateTypes(context: SchemaValidationContext): void { // Ensure Input Objects do not contain invalid default value circular references. validateInputObjectDefaultValueCircularRefs(type); + + // Ensure OneOf Input Objects are inhabitable. + if (type.isOneOf) { + validateOneOfInputObjectInhabitability(type); + } } } } @@ -943,6 +950,64 @@ function createInputObjectDefaultValueCircularRefsValidator( } } +function createOneOfInputObjectInhabitabilityValidator( + context: SchemaValidationContext, +): (inputObj: GraphQLInputObjectType) => void { + // Tracks already validated types to maintain O(N) across top-level calls. + const visitedTypes = new Set(); + + return function validateOneOfInputObjectInhabitability( + inputObj: GraphQLInputObjectType, + ): void { + if (visitedTypes.has(inputObj)) { + return; + } + + if (!isInhabitable(inputObj, new Set())) { + context.reportError( + `OneOf Input Object ${inputObj} must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.`, + inputObj.astNode, + ); + } + + visitedTypes.add(inputObj); + }; + + function isInhabitable( + inputObj: GraphQLInputObjectType, + visited: ReadonlySet, + ): boolean { + if (visited.has(inputObj)) { + return false; + } + + const nextVisited = new Set(visited); + nextVisited.add(inputObj); + + for (const field of Object.values(inputObj.getFields())) { + if (isListType(field.type)) { + return true; + } + + const namedType = getNamedType(field.type); + + if (!isInputObjectType(namedType)) { + return true; + } + + if (!namedType.isOneOf) { + return true; + } + + if (isInhabitable(namedType, nextVisited)) { + return true; + } + } + + return false; + } +} + function getAllImplementsInterfaceNodes( type: GraphQLObjectType | GraphQLInterfaceType, iface: GraphQLInterfaceType, From ba19546b6e00d8ae4ce9b6214816ab29db23ddf1 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Sat, 28 Feb 2026 04:51:24 -0800 Subject: [PATCH 2/2] Detect uninhabitable input types formed by OneOf and non-OneOf cycles Rework the circular references validation to detect input object types that cannot be provided a finite value. This covers: - Self-recursive OneOf types (e.g. input A @oneOf { a: A }) - Mixed OneOf/non-OneOf cycles with no escape path - Standard non-null circular references (existing behavior preserved) Rename algorithms to match spec terminology: - InputObjectHasUnbreakableCycle (was InputObjectCanBeProvidedAFiniteValue) - InputFieldTypeHasUnbreakableCycle (was FieldTypeCanBeProvidedAFiniteValue) --- src/type/__tests__/validation-test.ts | 207 +++++++++++++------- src/type/validate.ts | 259 ++++++++++++++------------ 2 files changed, 277 insertions(+), 189 deletions(-) diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 7c7a24660c..5786a114ec 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -924,7 +924,7 @@ describe('Type System: Input Objects must have fields', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Invalid circular reference. The Input Object SomeInputObject references itself in the non-null field SomeInputObject.nonNullSelf.', + 'Input Object SomeInputObject references itself via the required fields: SomeInputObject.nonNullSelf.', locations: [{ line: 7, column: 9 }], }, ]); @@ -952,13 +952,31 @@ describe('Type System: Input Objects must have fields', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Invalid circular reference. The Input Object SomeInputObject references itself via the non-null fields: SomeInputObject.startLoop, AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop.', + 'Input Object SomeInputObject references itself via the required fields: SomeInputObject.startLoop, AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop.', locations: [ { line: 7, column: 9 }, { line: 11, column: 9 }, { line: 15, column: 9 }, ], }, + { + message: + 'Input Object AnotherInputObject references itself via the required fields: AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop, SomeInputObject.startLoop.', + locations: [ + { line: 11, column: 9 }, + { line: 15, column: 9 }, + { line: 7, column: 9 }, + ], + }, + { + message: + 'Input Object YetAnotherInputObject references itself via the required fields: YetAnotherInputObject.closeLoop, SomeInputObject.startLoop, AnotherInputObject.nextInLoop.', + locations: [ + { line: 15, column: 9 }, + { line: 7, column: 9 }, + { line: 11, column: 9 }, + ], + }, ]); }); @@ -986,7 +1004,7 @@ describe('Type System: Input Objects must have fields', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Invalid circular reference. The Input Object SomeInputObject references itself via the non-null fields: SomeInputObject.startLoop, AnotherInputObject.closeLoop.', + 'Input Object SomeInputObject references itself via the required fields: SomeInputObject.startLoop, AnotherInputObject.closeLoop.', locations: [ { line: 7, column: 9 }, { line: 11, column: 9 }, @@ -994,16 +1012,20 @@ describe('Type System: Input Objects must have fields', () => { }, { message: - 'Invalid circular reference. The Input Object AnotherInputObject references itself via the non-null fields: AnotherInputObject.startSecondLoop, YetAnotherInputObject.closeSecondLoop.', + 'Input Object AnotherInputObject references itself via the required fields: AnotherInputObject.closeLoop, SomeInputObject.startLoop.', locations: [ - { line: 12, column: 9 }, - { line: 16, column: 9 }, + { line: 11, column: 9 }, + { line: 7, column: 9 }, ], }, { message: - 'Invalid circular reference. The Input Object YetAnotherInputObject references itself in the non-null field YetAnotherInputObject.nonNullSelf.', - locations: [{ line: 17, column: 9 }], + 'Input Object YetAnotherInputObject references itself via the required fields: YetAnotherInputObject.closeSecondLoop, AnotherInputObject.closeLoop, SomeInputObject.startLoop.', + locations: [ + { line: 16, column: 9 }, + { line: 11, column: 9 }, + { line: 7, column: 9 }, + ], }, ]); }); @@ -2409,173 +2431,202 @@ describe('Type System: OneOf Input Object fields must be nullable', () => { }); }); -describe('Type System: OneOf Input Objects must be inhabitable', () => { +describe('Type System: Input Objects must not have unbreakable cycles', () => { it('accepts a OneOf Input Object with a scalar field', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { - a: String - b: Int + a: Int } `); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts a OneOf Input Object with an enum field', () => { + it('accepts a OneOf Input Object with a recursive list field', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } - enum Color { RED GREEN BLUE } - input A @oneOf { - a: Color + a: [A!] } `); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts a OneOf Input Object with a list field', () => { + it('accepts a OneOf Input Object referencing a non-OneOf input object', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { - a: [A] + b: B + } + + input B { + x: Int } `); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts a OneOf Input Object referencing a non-OneOf input object', () => { + it('accepts a OneOf/OneOf cycle with a scalar escape', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { - a: RegularInput + b: B + escape: Int } - input RegularInput { - x: String + input B @oneOf { + a: A } `); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts a OneOf Input Object with at least one escape field', () => { + it('accepts a OneOf/non-OneOf cycle with a nullable escape', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { b: B - escape: String } - input B @oneOf { + input B { a: A } `); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts mutually referencing OneOf types where one has a scalar escape', () => { + it('rejects a self-referencing OneOf type with no escapes', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { - b: B - } - - input B @oneOf { - a: A - escape: Int + self: A } `); - expectJSON(validateSchema(schema)).toDeepEqual([]); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'Input Object A references itself via the required fields: A.self.', + locations: [{ line: 7, column: 9 }], + }, + ]); }); - it('accepts a OneOf referencing a non-OneOf which references back', () => { + it('rejects a mixed OneOf/non-OneOf cycle with no escapes', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { - b: RegularInput + b: B } - input RegularInput { - back: A + input B { + a: A! } `); - expectJSON(validateSchema(schema)).toDeepEqual([]); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'Input Object A references itself via the required fields: A.b, B.a.', + locations: [ + { line: 7, column: 9 }, + { line: 11, column: 9 }, + ], + }, + { + message: + 'Input Object B references itself via the required fields: B.a, A.b.', + locations: [ + { line: 11, column: 9 }, + { line: 7, column: 9 }, + ], + }, + ]); }); - it('accepts a OneOf with multiple fields where one escapes through chained OneOf types', () => { + it('accepts a OneOf/non-OneOf with scalar escape', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { b: B - c: C + escape: Int } - input B @oneOf { - a: A + input B { + a: A! } + `); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); - input C @oneOf { + it('accepts a non-OneOf/non-OneOf cycle with a nullable escape', () => { + const schema = buildSchema(` + type Query { + test(arg: A): Int + } + + input A { + b: B! + } + + input B { a: A - escape: String } `); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('rejects a closed subgraph of one OneOf type', () => { + it('accepts a non-OneOf/non-OneOf cycle with a list escape', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } - input A @oneOf { - self: A + input A { + b: [B!]! + } + + input B { + a: A! } `); - expectJSON(validateSchema(schema)).toDeepEqual([ - { - message: - 'OneOf Input Object A must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', - locations: [{ line: 6, column: 7 }], - }, - ]); + expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('rejects a closed subgraph of multiple OneOf types', () => { + it('rejects a larger mixed OneOf/non-OneOf cycle with no escapes', () => { const schema = buildSchema(` type Query { - test(arg: A): String + test(arg: A): Int } input A @oneOf { b: B } - input B @oneOf { - c: C + input B { + c: C! } input C @oneOf { @@ -2585,18 +2636,30 @@ describe('Type System: OneOf Input Objects must be inhabitable', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'OneOf Input Object A must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', - locations: [{ line: 6, column: 7 }], + 'Input Object A references itself via the required fields: A.b, B.c, C.a.', + locations: [ + { line: 7, column: 9 }, + { line: 11, column: 9 }, + { line: 15, column: 9 }, + ], }, { message: - 'OneOf Input Object B must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', - locations: [{ line: 10, column: 7 }], + 'Input Object B references itself via the required fields: B.c, C.a, A.b.', + locations: [ + { line: 11, column: 9 }, + { line: 15, column: 9 }, + { line: 7, column: 9 }, + ], }, { message: - 'OneOf Input Object C must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.', - locations: [{ line: 14, column: 7 }], + 'Input Object C references itself via the required fields: C.a, A.b, B.c.', + locations: [ + { line: 15, column: 9 }, + { line: 7, column: 9 }, + { line: 11, column: 9 }, + ], }, ]); }); diff --git a/src/type/validate.ts b/src/type/validate.ts index 4406fac3b0..7667616733 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -360,14 +360,15 @@ function validateName( } function validateTypes(context: SchemaValidationContext): void { - // Ensure Input Objects do not contain non-nullable circular references. - const validateInputObjectNonNullCircularRefs = - createInputObjectNonNullCircularRefsValidator(context); + const inputObjectUnbreakableCycleCheck = + createInputObjectUnbreakableCycleCheck(); const validateInputObjectDefaultValueCircularRefs = createInputObjectDefaultValueCircularRefsValidator(context); - const validateOneOfInputObjectInhabitability = - createOneOfInputObjectInhabitabilityValidator(context); const typeMap = context.schema.getTypeMap(); + + // Collect Input Object types that have unbreakable cycles. + const typesWithUnbreakableCycles = new Set(); + for (const type of Object.values(typeMap)) { // Ensure all provided types are in fact GraphQL type. if (!isNamedType(type)) { @@ -405,19 +406,25 @@ function validateTypes(context: SchemaValidationContext): void { // Ensure Input Object fields are valid. validateInputFields(context, type); - // Ensure Input Objects do not contain invalid field circular references. - // Ensure Input Objects do not contain non-nullable circular references. - validateInputObjectNonNullCircularRefs(type); + // Ensure Input Objects do not have unbreakable cycles. + if (inputObjectUnbreakableCycleCheck(type)) { + typesWithUnbreakableCycles.add(type); + } // Ensure Input Objects do not contain invalid default value circular references. validateInputObjectDefaultValueCircularRefs(type); - - // Ensure OneOf Input Objects are inhabitable. - if (type.isOneOf) { - validateOneOfInputObjectInhabitability(type); - } } } + + // Report errors for Input Object types that have unbreakable cycles. + for (const type of typesWithUnbreakableCycles) { + const cyclePath = traceUnbreakableCycle(type, typesWithUnbreakableCycles); + const pathStr = cyclePath.map((p) => p.fieldStr).join(', '); + context.reportError( + `Input Object ${type} references itself via the required fields: ${pathStr}.`, + cyclePath.map((p) => p.astNode), + ); + } } function validateFields( @@ -733,66 +740,142 @@ function validateOneOfInputObjectField( } } -function createInputObjectNonNullCircularRefsValidator( - context: SchemaValidationContext, -): (inputObj: GraphQLInputObjectType) => void { - // Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'. - // Tracks already visited types to maintain O(N) and to ensure that cycles - // are not redundantly reported. - const visitedTypes = new Set(); +// Implements the spec's InputObjectHasUnbreakableCycle algorithm. +// Tracks already checked types to maintain O(N) and to ensure that types +// are not redundantly checked. +function createInputObjectUnbreakableCycleCheck(): ( + inputObj: GraphQLInputObjectType, +) => boolean { + const knownNoCycle = new Set(); + const visited = new Set(); - // Array of types nodes used to produce meaningful errors - const fieldPath: Array<{ fieldStr: string; astNode: Maybe }> = []; + return inputObjectHasUnbreakableCycle; - // Position in the type path - const fieldPathIndexByTypeName: ObjMap = - Object.create(null); + function inputObjectHasUnbreakableCycle( + inputObj: GraphQLInputObjectType, + ): boolean { + if (knownNoCycle.has(inputObj)) { + return false; + } + if (visited.has(inputObj)) { + return true; + } - return detectCycleRecursive; + visited.add(inputObj); - // This does a straight-forward DFS to find cycles. - // It does not terminate when a cycle was found but continues to explore - // the graph to find all possible cycles. - function detectCycleRecursive(inputObj: GraphQLInputObjectType): void { - if (visitedTypes.has(inputObj)) { - return; + let result: boolean; + + if (inputObj.isOneOf) { + // OneOf Input Objects have an unbreakable cycle if every field has one. + result = true; + for (const field of Object.values(inputObj.getFields())) { + if (!inputFieldTypeHasUnbreakableCycle(field.type)) { + result = false; + break; + } + } + } else { + // Normal Input Objects have an unbreakable cycle if any non-null field has one. + result = false; + for (const field of Object.values(inputObj.getFields())) { + if ( + isNonNullType(field.type) && + inputFieldTypeHasUnbreakableCycle(field.type.ofType) + ) { + result = true; + break; + } + } } - visitedTypes.add(inputObj); - fieldPathIndexByTypeName[inputObj.name] = fieldPath.length; + visited.delete(inputObj); - const fields = Object.values(inputObj.getFields()); - for (const field of fields) { - if (isNonNullType(field.type) && isInputObjectType(field.type.ofType)) { - const fieldType = field.type.ofType; - const cycleIndex = fieldPathIndexByTypeName[fieldType.name]; + if (!result) { + knownNoCycle.add(inputObj); + } + return result; + } - fieldPath.push({ - fieldStr: `${inputObj}.${field.name}`, + function inputFieldTypeHasUnbreakableCycle( + fieldType: GraphQLInputType, + ): boolean { + if (isListType(fieldType)) { + return false; + } + if (isNonNullType(fieldType)) { + return inputFieldTypeHasUnbreakableCycle(fieldType.ofType); + } + if (!isInputObjectType(fieldType)) { + return false; + } + return inputObjectHasUnbreakableCycle(fieldType); + } +} + +// For an Input Object type with an unbreakable cycle, traces a witness cycle +// path by following required edges to other types with unbreakable cycles. +function traceUnbreakableCycle( + startType: GraphQLInputObjectType, + typesWithUnbreakableCycles: ReadonlySet, +): Array<{ fieldStr: string; astNode: Maybe }> { + const path: Array<{ fieldStr: string; astNode: Maybe }> = []; + const seen = new Set(); + + let current: Maybe = startType; + while (current != null && !seen.has(current)) { + seen.add(current); + let next: Maybe; + + for (const field of Object.values(current.getFields())) { + let target: Maybe; + + if (current.isOneOf) { + if ( + isInputObjectType(field.type) && + typesWithUnbreakableCycles.has(field.type) + ) { + target = field.type; + } + } else if (isNonNullType(field.type)) { + target = unwrapToUnbreakableCycleType( + field.type.ofType, + typesWithUnbreakableCycles, + ); + } + + if (target != null) { + path.push({ + fieldStr: `${current}.${field.name}`, astNode: field.astNode, }); - if (cycleIndex === undefined) { - detectCycleRecursive(fieldType); - } else { - const cyclePath = fieldPath.slice(cycleIndex); - const pathStr = cyclePath - .map((fieldObj) => fieldObj.fieldStr) - .join(', '); - context.reportError( - `Invalid circular reference. The Input Object ${fieldType} references itself ${ - cyclePath.length > 1 - ? 'via the non-null fields:' - : 'in the non-null field' - } ${pathStr}.`, - cyclePath.map((fieldObj) => fieldObj.astNode), - ); - } - fieldPath.pop(); + next = target; + break; } } - fieldPathIndexByTypeName[inputObj.name] = undefined; + current = next; + } + + return path; +} + +function unwrapToUnbreakableCycleType( + type: GraphQLInputType, + typesWithUnbreakableCycles: ReadonlySet, +): Maybe { + if (isListType(type)) { + return undefined; + } + if (isNonNullType(type)) { + return unwrapToUnbreakableCycleType( + type.ofType, + typesWithUnbreakableCycles, + ); + } + if (isInputObjectType(type) && typesWithUnbreakableCycles.has(type)) { + return type; } + return undefined; } function createInputObjectDefaultValueCircularRefsValidator( @@ -950,64 +1033,6 @@ function createInputObjectDefaultValueCircularRefsValidator( } } -function createOneOfInputObjectInhabitabilityValidator( - context: SchemaValidationContext, -): (inputObj: GraphQLInputObjectType) => void { - // Tracks already validated types to maintain O(N) across top-level calls. - const visitedTypes = new Set(); - - return function validateOneOfInputObjectInhabitability( - inputObj: GraphQLInputObjectType, - ): void { - if (visitedTypes.has(inputObj)) { - return; - } - - if (!isInhabitable(inputObj, new Set())) { - context.reportError( - `OneOf Input Object ${inputObj} must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.`, - inputObj.astNode, - ); - } - - visitedTypes.add(inputObj); - }; - - function isInhabitable( - inputObj: GraphQLInputObjectType, - visited: ReadonlySet, - ): boolean { - if (visited.has(inputObj)) { - return false; - } - - const nextVisited = new Set(visited); - nextVisited.add(inputObj); - - for (const field of Object.values(inputObj.getFields())) { - if (isListType(field.type)) { - return true; - } - - const namedType = getNamedType(field.type); - - if (!isInputObjectType(namedType)) { - return true; - } - - if (!namedType.isOneOf) { - return true; - } - - if (isInhabitable(namedType, nextVisited)) { - return true; - } - } - - return false; - } -} - function getAllImplementsInterfaceNodes( type: GraphQLObjectType | GraphQLInterfaceType, iface: GraphQLInterfaceType,