diff --git a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs index 4f9b0675ba7..ba1958d9d1b 100644 --- a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs @@ -60,9 +60,13 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex continue; } - ApplySemanticNonNullDirective(field, completionContext); + // TODO: This is not correct for lists + var hasSemanticNonNull = ApplySemanticNonNullDirective(field, completionContext); - field.FormatterDefinitions.Add(CreateSemanticNonNullResultFormatterDefinition()); + if (hasSemanticNonNull) + { + field.FormatterDefinitions.Add(CreateSemanticNonNullResultFormatterDefinition()); + } } } else if (definition is InterfaceTypeDefinition interfaceDef) @@ -80,20 +84,20 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex } } - private void ApplySemanticNonNullDirective( + private bool ApplySemanticNonNullDirective( OutputFieldDefinitionBase field, ITypeCompletionContext completionContext) { if (field.Type is null) { - return; + return false; } var levels = GetSemanticNonNullLevels(field.Type); if (levels.Count < 1) { - return; + return false; } var directiveDependency = new TypeDependency( @@ -105,6 +109,8 @@ private void ApplySemanticNonNullDirective( field.AddDirective(new SemanticNonNullDirective(levels), _typeInspector); field.Type = BuildNullableTypeStructure(field.Type, _typeInspector); + + return true; } private static List GetSemanticNonNullLevels(TypeReference typeReference) diff --git a/src/HotChocolate/Core/test/Execution.Tests/SemanticNonNullTests.cs b/src/HotChocolate/Core/test/Execution.Tests/SemanticNonNullTests.cs new file mode 100644 index 00000000000..e3281b1f916 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/SemanticNonNullTests.cs @@ -0,0 +1,490 @@ +using CookieCrumble; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Execution; + +public class SemanticNonNullTests +{ + [Fact] + public async Task NonPure_NonNull_Field_Returns_Null_Should_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + nonNullFieldReturningNull + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "TODO", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "nonNullFieldReturningNull" + ] + } + ], + "data": { + "nonNullFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task NonPure_NonNull_Field_Throwing_Should_Null_Field_And_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + nonNullFieldThrowingError + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "nonNullFieldThrowingError" + ] + } + ], + "data": { + "nonNullFieldThrowingError": null + } + } + """); + } + + [Fact] + public async Task NonPure_Nullable_Field_Returns_Null_Should_Null_Field_Without_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + nullableFieldReturningNull + } + """); + + result.MatchInlineSnapshot( + """ + { + "data": { + "nullableFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task NonPure_NonNull_Object_Field_Returns_Null_Should_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + nonNullObjectFieldReturningNull { + property + } + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "TODO", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "nonNullObjectFieldReturningNull" + ] + } + ], + "data": { + "nonNullObjectFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task NonPure_NonNull_Object_Field_Throwing_Should_Null_Field_And_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + nonNullObjectFieldThrowingError { + property + } + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "nonNullObjectFieldThrowingError" + ] + } + ], + "data": { + "nonNullObjectFieldThrowingError": null + } + } + """); + } + + [Fact] + public async Task NonPure_Nullable_Object_Field_Returns_Null_Should_Null_Field_Without_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + nullableObjectFieldReturningNull { + property + } + } + """); + + result.MatchInlineSnapshot( + """ + { + "data": { + "nullableObjectFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task Pure_NonNull_Field_Returns_Null_Should_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + pureNonNullFieldReturningNull + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "TODO", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "pureNonNullFieldReturningNull" + ] + } + ], + "data": { + "pureNonNullFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task Pure_NonNull_Field_Throwing_Should_Null_Field_And_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + pureNonNullFieldThrowingError + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "pureNonNullFieldThrowingError" + ] + } + ], + "data": { + "pureNonNullFieldThrowingError": null + } + } + """); + } + + [Fact] + public async Task Pure_Nullable_Field_Returns_Null_Should_Null_Field_Without_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + pureNullableFieldReturningNull + } + """); + + result.MatchInlineSnapshot( + """ + { + "data": { + "pureNullableFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task Pure_NonNull_Object_Field_Returns_Null_Should_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + pureNonNullObjectFieldReturningNull { + property + } + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "TODO", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "pureNonNullObjectFieldReturningNull" + ] + } + ], + "data": { + "pureNonNullObjectFieldReturningNull": null + } + } + """); + } + + [Fact] + public async Task Pure_NonNull_Object_Field_Throwing_Should_Null_Field_And_Produce_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + pureNonNullObjectFieldThrowingError { + property + } + } + """); + + result.MatchInlineSnapshot( + """ + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "pureNonNullObjectFieldThrowingError" + ] + } + ], + "data": { + "pureNonNullObjectFieldThrowingError": null + } + } + """); + } + + [Fact] + public async Task Pure_Nullable_Object_Field_Returns_Null_Should_Null_Field_Without_Error() + { + var result = await new ServiceCollection() + .AddGraphQL() + .ModifyOptions(o => + { + o.EnableSemanticNonNull = true; + }) + .AddQueryType() + .ExecuteRequestAsync(""" + { + pureNullableObjectFieldReturningNull { + property + } + } + """); + + result.MatchInlineSnapshot( + """ + { + "data": { + "pureNullableObjectFieldReturningNull": null + } + } + """); + } + + public class Query + { + public Task GetNonNullFieldReturningNull() + { + return Task.FromResult(null!); + } + + public Task GetNonNullFieldThrowingError() + { + throw new Exception("Something went wrong"); + } + + public Task GetNullableFieldReturningNull() + { + return Task.FromResult(null); + } + + public Task GetNonNullObjectFieldReturningNull() + { + return Task.FromResult(null!); + } + + public Task GetNonNullObjectFieldThrowingError() + { + throw new Exception("Something went wrong"); + } + + public Task GetNullableObjectFieldReturningNull() + { + return Task.FromResult(null); + } + + public string PureNonNullFieldReturningNull => null!; + + public string PureNonNullFieldThrowingError => throw new Exception("Somethin went wrong"); + + public string? PureNullableFieldReturningNull => null; + + public SomeObject PureNonNullObjectFieldReturningNull => null!; + + public SomeObject PureNonNullObjectFieldThrowingError => throw new Exception("Somethin went wrong"); + + public SomeObject? PureNullableObjectFieldReturningNull => null; + } + + public record SomeObject(string Property); +}