Skip to content

[Fusion] @semanticNonNull support #7894

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

Merged
merged 3 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
using HotChocolate.Types.Helpers;
using HotChocolate.Types.Pagination;
using HotChocolate.Types.Relay;
using HotChocolate.Utilities;

namespace HotChocolate;

Expand Down Expand Up @@ -346,7 +346,7 @@ private static void CheckResultForSemanticNonNullViolations(object? result, IRes
{
if (result is null && levels.Contains(currentLevel))
{
context.ReportError(CreateSemanticNonNullViolationError(path));
context.ReportError(ErrorHelper.CreateSemanticNonNullViolationError(path, context.Selection));
return;
}

Expand All @@ -367,11 +367,4 @@ private static void CheckResultForSemanticNonNullViolations(object? result, IRes
}
}
}

private static IError CreateSemanticNonNullViolationError(Path path)
=> ErrorBuilder.New()
.SetMessage("Cannot return null for semantic non-null field.")
.SetCode(ErrorCodes.Execution.SemanticNonNullViolation)
.SetPath(path)
.Build();
}
9 changes: 9 additions & 0 deletions src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Properties;
using HotChocolate.Types;
Expand Down Expand Up @@ -538,4 +539,12 @@ public static ISchemaError DuplicateFieldName(
.SetTypeSystemObject(type)
.Build();
}

public static IError CreateSemanticNonNullViolationError(Path path, ISelection selection)
=> ErrorBuilder.New()
.SetMessage("Cannot return null for semantic non-null field.")
.SetCode(ErrorCodes.Execution.SemanticNonNullViolation)
.AddLocation(selection.SyntaxNode)
.SetPath(path)
.Build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public static FusionGatewayBuilder AddFusionGatewayServer(
.UseField(next => next)
.AddOperationCompilerOptimizer<OperationQueryPlanCompiler>()
.AddOperationCompilerOptimizer<FieldFlagsOptimizer>()
.AddOperationCompilerOptimizer<SemanticNonNullOptimizer>()
.AddConvention<INamingConventions>(_ => new DefaultNamingConventions())
.ModifyCostOptions(o => o.ApplyCostDefaults = false)
.Configure(
Expand Down
159 changes: 121 additions & 38 deletions src/HotChocolate/Fusion/src/Core/Execution/ExecutionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Json;
using HotChocolate.Execution.Processing;
using HotChocolate.Fusion.Execution.Nodes;
using HotChocolate.Fusion.Execution.Pipeline;
using HotChocolate.Fusion.Metadata;
using HotChocolate.Fusion.Planning;
using HotChocolate.Fusion.Utilities;
Expand All @@ -12,6 +13,7 @@
using HotChocolate.Types;
using HotChocolate.Utilities;
using static HotChocolate.Execution.Processing.Selection;
using ErrorHelper = HotChocolate.Utilities.ErrorHelper;
using IType = HotChocolate.Types.IType;
using ObjectType = HotChocolate.Types.ObjectType;

Expand Down Expand Up @@ -41,7 +43,8 @@ private static void ComposeResult(
SelectionSet selectionSet,
SelectionData[] selectionSetData,
ObjectResult selectionSetResult,
bool partialResult = false)
bool partialResult = false,
int level = 0)
{
if (selectionSetResult.IsInvalidated)
{
Expand Down Expand Up @@ -69,14 +72,19 @@ private static void ComposeResult(

if (!field.IsIntrospectionField)
{
var nullable = selection.TypeKind is not TypeKind.NonNull;
var namedType = selectionType.NamedType();
var isSemanticNonNull = IsSemanticNonNull(selection, level);
var nullable = selectionType.IsNullableType();
var nullableType = selectionType.NullableType();

if (!data.HasValue)
{
if (!partialResult)
{
if (!nullable)
if (isSemanticNonNull)
{
AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex);
}
else if (!nullable)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
Expand All @@ -85,14 +93,21 @@ private static void ComposeResult(
result.Set(responseName, null, nullable);
}
}
else if (namedType.IsType(TypeKind.Scalar))
else if (nullableType.IsType(TypeKind.Scalar))
{
var value = data.Single.Element;

if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined && !nullable)
if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
if (isSemanticNonNull)
{
AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex);
}
else if (!nullable)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
}
}

result.Set(responseName, value, nullable);
Expand All @@ -105,15 +120,21 @@ private static void ComposeResult(
result.Set(responseName, reformattedId, nullable);
}
}
else if (namedType.IsType(TypeKind.Enum))
else if (nullableType.IsType(TypeKind.Enum))
{
// we might need to map the enum value!
var value = data.Single.Element;

if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined && !nullable)
if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
if (isSemanticNonNull)
{
AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex);
}
else if (!nullable)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
}
}

result.Set(responseName, value, nullable);
Expand All @@ -122,7 +143,7 @@ private static void ComposeResult(
{
if (!result.IsInitialized)
{
// we add a placeholder here so if the ComposeObject propagates an error
// we add a placeholder here so if ComposeObject propagates an error
// there is a value here.
result.Set(responseName, null, nullable);

Expand All @@ -131,12 +152,20 @@ private static void ComposeResult(
selectionSetResult,
responseIndex,
selection,
data);
data,
level);

if (value is null && !nullable)
if (value is null)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
if (isSemanticNonNull)
{
AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex);
}
else if (!nullable)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
}
}

result.Set(responseName, value, nullable);
Expand All @@ -146,18 +175,30 @@ private static void ComposeResult(
{
if (!result.IsInitialized)
{
// we add a placeholder here so if ComposeList propagates an error
// there is a value here.
result.Set(responseName, null, nullable);

var value = ComposeList(
context,
selectionSetResult,
responseIndex,
selection,
data,
selectionType);
selectionType,
level + 1);

if (value is null && !nullable)
if (value is null)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
if (isSemanticNonNull)
{
AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex);
}
else if (!nullable)
{
PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex);
break;
}
}

result.Set(responseName, value, nullable);
Expand Down Expand Up @@ -191,7 +232,8 @@ private static void ComposeResult(
int parentIndex,
Selection selection,
SelectionData selectionData,
IType type)
IType type,
int level)
{
if (selectionData.IsNull())
{
Expand All @@ -206,14 +248,15 @@ private static void ComposeResult(
var index = 0;
var elementType = type.ElementType();
var nullable = elementType.IsNullableType();
var isSemanticNonNull = IsSemanticNonNull(selection, level);
var result = context.Result.RentList(json.GetArrayLength());

result.IsNullable = nullable;
result.SetParent(parent, parentIndex);

foreach (var item in json.EnumerateArray())
{
// we add a placeholder here so if the ComposeElement propagates an error
// we add a placeholder here so if ComposeElement propagates an error
// there is a value here.
result.AddUnsafe(null);

Expand All @@ -223,12 +266,20 @@ private static void ComposeResult(
index,
selection,
new SelectionData(new JsonResult(schemaName, item)),
elementType);
elementType,
level);

if (!nullable && element is null)
if (element is null)
{
PropagateNullValues(context.Result, selection, result, index);
return null;
if (isSemanticNonNull)
{
AddSemanticNonNullViolation(context.Result, selection, result, index);
}
else if (!nullable)
{
PropagateNullValues(context.Result, selection, result, index);
break;
}
}

result.SetUnsafe(index++, element);
Expand All @@ -248,16 +299,17 @@ private static void ComposeResult(
int parentIndex,
Selection selection,
SelectionData selectionData,
IType valueType)
IType valueType,
int level)
{
var namedType = valueType.NamedType();
var nullableType = valueType.NullableType();

if (!selectionData.HasValue)
{
return null;
}

if (namedType.IsType(TypeKind.Scalar))
if (nullableType.IsType(TypeKind.Scalar))
{
var value = selectionData.Single.Element;

Expand All @@ -276,7 +328,7 @@ private static void ComposeResult(
return value;
}

if (namedType.IsType(TypeKind.Enum))
if (nullableType.IsType(TypeKind.Enum))
{
var value = selectionData.Single.Element;

Expand All @@ -288,17 +340,18 @@ private static void ComposeResult(
return value;
}

return TypeExtensions.IsCompositeType(valueType)
? ComposeObject(context, parent, parentIndex, selection, selectionData)
: ComposeList(context, parent, parentIndex, selection, selectionData, valueType);
return nullableType.IsCompositeType()
? ComposeObject(context, parent, parentIndex, selection, selectionData, 0)
: ComposeList(context, parent, parentIndex, selection, selectionData, valueType, level + 1);
}

private static ObjectResult? ComposeObject(
FusionExecutionContext context,
ResultData parent,
int parentIndex,
ISelection selection,
SelectionData selectionData)
SelectionData selectionData,
int level)
{
if (selectionData.IsNull())
{
Expand Down Expand Up @@ -330,13 +383,13 @@ private static void ComposeResult(

var childSelectionResults = new SelectionData[selectionCount];
ExtractSelectionResults(selectionData, selectionSet, childSelectionResults);
ComposeResult(context, selectionSet, childSelectionResults, result, true);
ComposeResult(context, selectionSet, childSelectionResults, result, true, level);
}
else
{
var childSelectionResults = new SelectionData[selectionCount];
ExtractSelectionResults(selectionData, selectionSet, childSelectionResults);
ComposeResult(context, selectionSet, childSelectionResults, result);
ComposeResult(context, selectionSet, childSelectionResults, result, false, level);
}

return result.IsInvalidated ? null : result;
Expand Down Expand Up @@ -716,6 +769,17 @@ public static void ExtractVariables(
}
}

private static void AddSemanticNonNullViolation(
ResultBuilder resultBuilder,
Selection selection,
ResultData selectionSetResult,
int responseIndex)
{
var path = PathHelper.CreatePathFromContext(selection, selectionSetResult, responseIndex);
var error = ErrorHelper.CreateSemanticNonNullViolationError(path, selection);
resultBuilder.AddError(error);
}

private static void PropagateNullValues(
ResultBuilder resultBuilder,
Selection selection,
Expand All @@ -727,6 +791,25 @@ private static void PropagateNullValues(
ValueCompletion.PropagateNullValues(selectionSetResult);
}

private static readonly CustomOptionsFlags[] _levelOptions =
[
CustomOptionsFlags.Option5,
CustomOptionsFlags.Option6,
CustomOptionsFlags.Option7
];

private static bool IsSemanticNonNull(Selection selection, int level)
{
if (level >= SemanticNonNullOptimizer.MaxLevels)
{
return true;
}

var optionForLevel = _levelOptions[level];

return selection.CustomOptions.HasFlag(optionForLevel);
}

private sealed class ErrorPathContext
{
public string Current { get; set; } = string.Empty;
Expand Down
Loading
Loading