Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add circular reference validation rules in input types #6821

Merged
merged 10 commits into from
Jan 16, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using HotChocolate.Language;
using HotChocolate.Types;
using static HotChocolate.Configuration.Validation.TypeValidationHelper;
using static HotChocolate.Utilities.ErrorHelper;
Expand All @@ -15,18 +18,104 @@
IReadOnlySchemaOptions options,
ICollection<ISchemaError> errors)
{
if (options.StrictValidation)
if (!options.StrictValidation)
{
List<string>? names = null;
return;
}

List<string>? names = null;
CycleValidationContext cycleValidationContext = new()
{
Visited = new(),
CycleStartIndex = new(),
Errors = errors,
FieldPath = new(),
};

foreach (var type in typeSystemObjects)
{
if (type is not InputObjectType inputType)
{
continue;
}

EnsureTypeHasFields(inputType, errors);
EnsureFieldNamesAreValid(inputType, errors);
EnsureOneOfFieldsAreValid(inputType, errors, ref names);
EnsureFieldDeprecationIsValid(inputType, errors);
TryReachCycleRecursively(cycleValidationContext, inputType);

cycleValidationContext.CycleStartIndex.Clear();
}

Check warning on line 49 in src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs

View check run for this annotation

Codecov / codecov/patch

src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs#L47-L49

Added lines #L47 - L49 were not covered by tests
}

private struct CycleValidationContext
{
public HashSet<InputObjectType> Visited { get; set; }

Check warning on line 54 in src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs

View check run for this annotation

Codecov / codecov/patch

src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs#L53-L54

Added lines #L53 - L54 were not covered by tests
public Dictionary<InputObjectType, int> CycleStartIndex { get; set; }
public ICollection<ISchemaError> Errors { get; set; }
public List<string> FieldPath { get; set; }
}

Check warning on line 59 in src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs

View check run for this annotation

Codecov / codecov/patch

src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs#L56-L59

Added lines #L56 - L59 were not covered by tests
// https://github.com/IvanGoncharov/graphql-js/blob/408bcda9c88df85e039f5d072011b1cb465fe830/src/type/validate.js#L535
private static void TryReachCycleRecursively(
in CycleValidationContext context,
InputObjectType type)

Check warning on line 63 in src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs

View check run for this annotation

Codecov / codecov/patch

src/HotChocolate/Core/src/Types/Configuration/Validation/InputObjectTypeValidationRule.cs#L61-L63

Added lines #L61 - L63 were not covered by tests
{
if (!context.Visited.Add(type))
{
return;
}

context.CycleStartIndex[type] = context.FieldPath.Count;

foreach (var field in type.Fields)
{
var unwrappedType = UnwrapCompletelyIfRequired(field.Type);
if (unwrappedType is not InputObjectType inputObjectType)
{
continue;
}

context.FieldPath.Add(field.Name);
if (context.CycleStartIndex.TryGetValue(inputObjectType, out var cycleIndex))
{
var cyclePath = context.FieldPath.Skip(cycleIndex);
context.Errors.Add(
InputObjectMustNotHaveRecursiveNonNullableReferencesToSelf(type, cyclePath));
}
else
{
TryReachCycleRecursively(context, inputObjectType);
}
context.FieldPath.Pop();
}

context.CycleStartIndex.Remove(type);
}

private static IType? UnwrapCompletelyIfRequired(IType type)
{
while (true)
{
if (type.Kind == TypeKind.NonNull)
{
type = ((NonNullType)type).Type;
}
else
{
return null;
}

foreach (var type in typeSystemObjects)
switch (type.Kind)
{
if (type is InputObjectType inputType)
case TypeKind.List:
{
EnsureTypeHasFields(inputType, errors);
EnsureFieldNamesAreValid(inputType, errors);
EnsureOneOfFieldsAreValid(inputType, errors, ref names);
EnsureFieldDeprecationIsValid(inputType, errors);
return null;
}
default:
{
return type;
}
}
}
Expand All @@ -37,30 +126,33 @@
ICollection<ISchemaError> errors,
ref List<string>? temp)
{
if (type.Directives.ContainsDirective(WellKnownDirectives.OneOf))
if (!type.Directives.ContainsDirective(WellKnownDirectives.OneOf))
{
temp ??= new List<string>();
return;
}

foreach (var field in type.Fields)
{
if (field.Type.Kind is TypeKind.NonNull || field.DefaultValue is not null)
{
temp.Add(field.Name);
}
}
temp ??= new List<string>();

if (temp.Count > 0)
foreach (var field in type.Fields)
{
if (field.Type.Kind is TypeKind.NonNull || field.DefaultValue is not null)
{
var fieldNames = new string[temp.Count];
temp.Add(field.Name);
}
}

for (var i = 0; i < temp.Count; i++)
{
fieldNames[i] = temp[i];
}
if (temp.Count == 0)
{
return;
}

temp.Clear();
errors.Add(OneofInputObjectMustHaveNullableFieldsWithoutDefaults(type, fieldNames));
}
var fieldNames = new string[temp.Count];
for (var i = 0; i < temp.Count; i++)
{
fieldNames[i] = temp[i];
}

temp.Clear();
errors.Add(OneofInputObjectMustHaveNullableFieldsWithoutDefaults(type, fieldNames));
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.