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,103 @@ internal sealed class InputObjectTypeValidationRule : ISchemaValidationRule
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)
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
{
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();
}
}

private struct CycleValidationContext
{
public HashSet<InputObjectType> Visited { get; set; }
public Dictionary<InputObjectType, int> CycleStartIndex { get; set; }
public ICollection<ISchemaError> Errors { get; set; }
public List<string> FieldPath { get; set; }
}

// https://github.com/IvanGoncharov/graphql-js/blob/408bcda9c88df85e039f5d072011b1cb465fe830/src/type/validate.js#L535
private static void TryReachCycleRecursively(
in CycleValidationContext context,
InputObjectType type)
{
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 +125,33 @@ internal sealed class InputObjectTypeValidationRule : ISchemaValidationRule
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.