Skip to content

Commit b50b6c5

Browse files
AntonC9018michaelstaib
authored andcommitted
Added circular reference validation rules in input types (#6821)
1 parent 53a5fec commit b50b6c5

File tree

12 files changed

+638
-407
lines changed

12 files changed

+638
-407
lines changed

src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs

Lines changed: 118 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
using System;
44
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Linq;
7+
using HotChocolate.Language;
58
using HotChocolate.Types;
69
using static HotChocolate.Configuration.Validation.TypeValidationHelper;
710
using static HotChocolate.Utilities.ErrorHelper;
@@ -15,18 +18,104 @@ public void Validate(
1518
IReadOnlySchemaOptions options,
1619
ICollection<ISchemaError> errors)
1720
{
18-
if (options.StrictValidation)
21+
if (!options.StrictValidation)
1922
{
20-
List<string>? names = null;
23+
return;
24+
}
25+
26+
List<string>? names = null;
27+
CycleValidationContext cycleValidationContext = new()
28+
{
29+
Visited = new(),
30+
CycleStartIndex = new(),
31+
Errors = errors,
32+
FieldPath = new(),
33+
};
34+
35+
foreach (var type in typeSystemObjects)
36+
{
37+
if (type is not InputObjectType inputType)
38+
{
39+
continue;
40+
}
41+
42+
EnsureTypeHasFields(inputType, errors);
43+
EnsureFieldNamesAreValid(inputType, errors);
44+
EnsureOneOfFieldsAreValid(inputType, errors, ref names);
45+
EnsureFieldDeprecationIsValid(inputType, errors);
46+
TryReachCycleRecursively(cycleValidationContext, inputType);
47+
48+
cycleValidationContext.CycleStartIndex.Clear();
49+
}
50+
}
51+
52+
private struct CycleValidationContext
53+
{
54+
public HashSet<InputObjectType> Visited { get; set; }
55+
public Dictionary<InputObjectType, int> CycleStartIndex { get; set; }
56+
public ICollection<ISchemaError> Errors { get; set; }
57+
public List<string> FieldPath { get; set; }
58+
}
59+
60+
// https://github.com/IvanGoncharov/graphql-js/blob/408bcda9c88df85e039f5d072011b1cb465fe830/src/type/validate.js#L535
61+
private static void TryReachCycleRecursively(
62+
in CycleValidationContext context,
63+
InputObjectType type)
64+
{
65+
if (!context.Visited.Add(type))
66+
{
67+
return;
68+
}
69+
70+
context.CycleStartIndex[type] = context.FieldPath.Count;
71+
72+
foreach (var field in type.Fields)
73+
{
74+
var unwrappedType = UnwrapCompletelyIfRequired(field.Type);
75+
if (unwrappedType is not InputObjectType inputObjectType)
76+
{
77+
continue;
78+
}
79+
80+
context.FieldPath.Add(field.Name);
81+
if (context.CycleStartIndex.TryGetValue(inputObjectType, out var cycleIndex))
82+
{
83+
var cyclePath = context.FieldPath.Skip(cycleIndex);
84+
context.Errors.Add(
85+
InputObjectMustNotHaveRecursiveNonNullableReferencesToSelf(type, cyclePath));
86+
}
87+
else
88+
{
89+
TryReachCycleRecursively(context, inputObjectType);
90+
}
91+
context.FieldPath.Pop();
92+
}
93+
94+
context.CycleStartIndex.Remove(type);
95+
}
96+
97+
private static IType? UnwrapCompletelyIfRequired(IType type)
98+
{
99+
while (true)
100+
{
101+
if (type.Kind == TypeKind.NonNull)
102+
{
103+
type = ((NonNullType)type).Type;
104+
}
105+
else
106+
{
107+
return null;
108+
}
21109

22-
foreach (var type in typeSystemObjects)
110+
switch (type.Kind)
23111
{
24-
if (type is InputObjectType inputType)
112+
case TypeKind.List:
25113
{
26-
EnsureTypeHasFields(inputType, errors);
27-
EnsureFieldNamesAreValid(inputType, errors);
28-
EnsureOneOfFieldsAreValid(inputType, errors, ref names);
29-
EnsureFieldDeprecationIsValid(inputType, errors);
114+
return null;
115+
}
116+
default:
117+
{
118+
return type;
30119
}
31120
}
32121
}
@@ -37,30 +126,33 @@ private static void EnsureOneOfFieldsAreValid(
37126
ICollection<ISchemaError> errors,
38127
ref List<string>? temp)
39128
{
40-
if (type.Directives.ContainsDirective(WellKnownDirectives.OneOf))
129+
if (!type.Directives.ContainsDirective(WellKnownDirectives.OneOf))
41130
{
42-
temp ??= new List<string>();
131+
return;
132+
}
43133

44-
foreach (var field in type.Fields)
45-
{
46-
if (field.Type.Kind is TypeKind.NonNull || field.DefaultValue is not null)
47-
{
48-
temp.Add(field.Name);
49-
}
50-
}
134+
temp ??= new List<string>();
51135

52-
if (temp.Count > 0)
136+
foreach (var field in type.Fields)
137+
{
138+
if (field.Type.Kind is TypeKind.NonNull || field.DefaultValue is not null)
53139
{
54-
var fieldNames = new string[temp.Count];
140+
temp.Add(field.Name);
141+
}
142+
}
55143

56-
for (var i = 0; i < temp.Count; i++)
57-
{
58-
fieldNames[i] = temp[i];
59-
}
144+
if (temp.Count == 0)
145+
{
146+
return;
147+
}
60148

61-
temp.Clear();
62-
errors.Add(OneofInputObjectMustHaveNullableFieldsWithoutDefaults(type, fieldNames));
63-
}
149+
var fieldNames = new string[temp.Count];
150+
for (var i = 0; i < temp.Count; i++)
151+
{
152+
fieldNames[i] = temp[i];
64153
}
154+
155+
temp.Clear();
156+
errors.Add(OneofInputObjectMustHaveNullableFieldsWithoutDefaults(type, fieldNames));
65157
}
66158
}

src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)