Skip to content
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 @@ -78,6 +78,8 @@ public override void WriteResolverFields(IOutputTypeInfo type)
{
WriteResolverField(objectType.NodeResolver);
}

WriteIsSelectedFields(objectType.NodeResolver);
}
}

Expand All @@ -93,7 +95,9 @@ public override void WriteResolverConstructor(IOutputTypeInfo type, ILocalTypeLo
objectType,
typeLookup,
type.Resolvers.Any(t => t.RequiresParameterBindings)
|| (objectType.NodeResolver?.RequiresParameterBindings ?? false));
|| (objectType.NodeResolver?.RequiresParameterBindings ?? false),
type.Resolvers.Any(HasIsSelectedFields)
|| HasIsSelectedFields(objectType.NodeResolver));
}

protected override void WriteResolversBindingInitialization(IOutputTypeInfo type, ILocalTypeLookup typeLookup)
Expand All @@ -112,6 +116,8 @@ protected override void WriteResolversBindingInitialization(IOutputTypeInfo type
{
WriteResolverBindingInitialization(objectType.NodeResolver, typeLookup);
}

WriteIsSelectedInitialization(objectType.NodeResolver);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ public virtual void WriteResolverFields(IOutputTypeInfo type)
{
WriteResolverField(resolver);
}

WriteIsSelectedFields(resolver);
}
}

Expand All @@ -692,23 +694,34 @@ public virtual void WriteResolverConstructor(IOutputTypeInfo type, ILocalTypeLoo
WriteResolverConstructor(
type,
typeLookup,
type.Resolvers.Any(t => t.RequiresParameterBindings));
type.Resolvers.Any(t => t.RequiresParameterBindings),
type.Resolvers.Any(HasIsSelectedFields));
}

protected void WriteResolverConstructor(
IOutputTypeInfo type,
ILocalTypeLookup typeLookup,
bool requiresParameterBindings)
bool requiresParameterBindings,
bool requiresIsSelectedInit = false)
{
if (!requiresParameterBindings)
if (!requiresParameterBindings && !requiresIsSelectedInit)
{
return;
}

Writer.WriteLine();
Writer.WriteIndentedLine(
"public __Resolvers(global::{0} bindingResolver)",
WellKnownTypes.ParameterBindingResolver);

if (requiresParameterBindings)
{
Writer.WriteIndentedLine(
"public __Resolvers(global::{0} bindingResolver)",
WellKnownTypes.ParameterBindingResolver);
}
else
{
Writer.WriteIndentedLine("public __Resolvers()");
}

Writer.WriteIndentedLine("{");

using (Writer.IncreaseIndent())
Expand All @@ -725,6 +738,7 @@ protected virtual void WriteResolversBindingInitialization(IOutputTypeInfo type,
foreach (var resolver in type.Resolvers)
{
WriteResolverBindingInitialization(resolver, typeLookup);
WriteIsSelectedInitialization(resolver);
}
}

Expand Down Expand Up @@ -1700,6 +1714,63 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet
i);
break;

case ResolverParameterKind.IsSelected:
var (variant, fieldNames, _) = GetIsSelectedInfo(parameter);

switch (variant)
{
case IsSelectedVariant.SingleField:
Writer.WriteIndentedLine(
"var args{0} = context.Select().IsSelected(\"{1}\");",
i,
fieldNames[0]);
break;

case IsSelectedVariant.MultipleFields:
var sb = new StringBuilder();
for (var j = 0; j < fieldNames.Length; j++)
{
if (j > 0)
{
sb.Append(", ");
}

sb.Append('"');
sb.Append(fieldNames[j]);
sb.Append('"');
}

Writer.WriteIndentedLine(
"var args{0} = context.Select().IsSelected({1});",
i,
sb.ToString());
break;

case IsSelectedVariant.FieldSet:
Writer.WriteIndentedLine(
"var args{0} = context.Select().IsSelected(_isSelected_{1}_{2});",
i,
resolver.Member.Name,
parameter.Name);
break;

case IsSelectedVariant.Pattern:
Writer.WriteIndentedLine(
"var args{0}_selectionContext = new global::HotChocolate.Resolvers.IsSelectedContext(context.Schema, context.Select());",
i);
Writer.WriteIndentedLine(
"global::HotChocolate.Resolvers.IsSelectedVisitor.Instance.Visit(_isSelected_{0}_{1}, args{2}_selectionContext);",
resolver.Member.Name,
parameter.Name,
i);
Writer.WriteIndentedLine(
"var args{0} = args{0}_selectionContext.AllSelected;",
i);
break;
}
Comment on lines +1717 to +1770
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResolverParameterKind.IsSelected is handled here for non-batch resolvers, but the batch-resolver argument generation switch (in WriteBatchResolver) does not handle IsSelected and will fall back to default(...), causing batch resolvers to always receive false (or the default) for [IsSelected] parameters. Add corresponding handling in the batch-resolver path (likely mirroring the Selection case using contexts[0]).

Copilot uses AI. Check for mistakes.

break;

case ResolverParameterKind.Unknown:
Writer.WriteIndentedLine(
"var args{0} = _binding_{1}_{2}.Execute<{3}>(context);",
Expand Down Expand Up @@ -1966,6 +2037,165 @@ private static string EscapeChar(char c)
};
}

protected void WriteIsSelectedFields(Resolver resolver)
{
foreach (var parameter in resolver.Parameters)
{
if (parameter.Kind != ResolverParameterKind.IsSelected)
{
continue;
}

var (variant, _, _) = GetIsSelectedInfo(parameter);

switch (variant)
{
case IsSelectedVariant.FieldSet:
Writer.WriteIndentedLine(
"private readonly global::System.Collections.Generic.HashSet<string> _isSelected_{0}_{1};",
resolver.Member.Name,
parameter.Name);
break;

case IsSelectedVariant.Pattern:
Writer.WriteIndentedLine(
"private readonly global::HotChocolate.Language.SelectionSetNode _isSelected_{0}_{1};",
resolver.Member.Name,
parameter.Name);
break;
}
}
}

protected void WriteIsSelectedInitialization(Resolver resolver)
{
foreach (var parameter in resolver.Parameters)
{
if (parameter.Kind != ResolverParameterKind.IsSelected)
{
continue;
}

var (variant, fieldNames, patternString) = GetIsSelectedInfo(parameter);

switch (variant)
{
case IsSelectedVariant.FieldSet:
var sb = new StringBuilder();
for (var i = 0; i < fieldNames.Length; i++)
{
if (i > 0)
{
sb.Append(", ");
}

sb.Append('"');
sb.Append(fieldNames[i]);
sb.Append('"');
}

Writer.WriteIndentedLine(
"_isSelected_{0}_{1} = new global::System.Collections.Generic.HashSet<string>([{2}]);",
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated HashSet initialization uses a collection expression ([...]), which requires C# 12. Source generator output should generally compile under the consumer project's language version; using new[] { ... } / new string[] { ... } here would avoid introducing a hard C# 12 dependency.

Suggested change
"_isSelected_{0}_{1} = new global::System.Collections.Generic.HashSet<string>([{2}]);",
"_isSelected_{0}_{1} = new global::System.Collections.Generic.HashSet<string>(new string[] {{ {2} }});",

Copilot uses AI. Check for mistakes.
resolver.Member.Name,
parameter.Name,
sb.ToString());
break;

case IsSelectedVariant.Pattern:
Writer.WriteIndentedLine(
$"_isSelected_{resolver.Member.Name}_{parameter.Name} = global::HotChocolate.Language.Utf8GraphQLParser.Syntax.ParseSelectionSet(\"{{ {patternString} }}\");");
Comment on lines +2105 to +2106
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patternString is emitted directly into a C# string literal for ParseSelectionSet(...) without escaping. If the pattern includes quotes/backslashes/newlines (e.g. field arguments or directives with string values), the generated source will not compile; please escape the value (you already have EscapeString(...) in this file).

Suggested change
Writer.WriteIndentedLine(
$"_isSelected_{resolver.Member.Name}_{parameter.Name} = global::HotChocolate.Language.Utf8GraphQLParser.Syntax.ParseSelectionSet(\"{{ {patternString} }}\");");
var selectionSet = EscapeString($"{{ {patternString} }}");
Writer.WriteIndentedLine(
$"_isSelected_{resolver.Member.Name}_{parameter.Name} = global::HotChocolate.Language.Utf8GraphQLParser.Syntax.ParseSelectionSet(\"{selectionSet}\");");

Copilot uses AI. Check for mistakes.
break;
}
}
}

protected static bool HasIsSelectedFields(Resolver? resolver)
{
if (resolver is null)
{
return false;
}

foreach (var parameter in resolver.Parameters)
{
if (parameter.Kind != ResolverParameterKind.IsSelected)
{
continue;
}

var (variant, _, _) = GetIsSelectedInfo(parameter);
if (variant is IsSelectedVariant.FieldSet or IsSelectedVariant.Pattern)
{
return true;
}
}

return false;
}

private static (IsSelectedVariant Variant, string[] FieldNames, string? PatternString) GetIsSelectedInfo(
ResolverParameter parameter)
{
AttributeData? attr = null;
foreach (var a in parameter.Attributes)
{
if (a.AttributeClass?.ToDisplayString() == WellKnownAttributes.IsSelectedAttribute)
{
attr = a;
break;
}
}

if (attr is null)
{
throw new InvalidOperationException("The parameter does not have an IsSelectedAttribute.");
}

var args = attr.ConstructorArguments;

// params string[] constructor (4+ fields)
if (args.Length == 1 && args[0].Kind == TypedConstantKind.Array)
{
var names = new string[args[0].Values.Length];
for (var i = 0; i < names.Length; i++)
{
names[i] = (string)args[0].Values[i].Value!;
}

return (IsSelectedVariant.FieldSet, names, null);
}

// Single string constructor
if (args.Length == 1)
{
var fieldName = (string)args[0].Value!;

if (fieldName.IndexOf(' ') < 0 && fieldName.IndexOf('{') < 0)
{
return (IsSelectedVariant.SingleField, [fieldName], null);
}

return (IsSelectedVariant.Pattern, [], fieldName);
}
Comment on lines +2168 to +2179
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SingleField vs Pattern detection for the single-string ctor argument only checks for spaces / {. A value like name(arg: "x"), name@skip(if:true), or other non-GraphQL-Name characters would be treated as SingleField and generate IsSelected("...") (likely always false) instead of being parsed as a selection set. Consider using a proper GraphQL Name check (or parsing) to decide the variant.

Copilot uses AI. Check for mistakes.

// 2 or 3 explicit string args
var fieldNames = new string[args.Length];
for (var i = 0; i < fieldNames.Length; i++)
{
fieldNames[i] = (string)args[i].Value!;
}

return (IsSelectedVariant.MultipleFields, fieldNames, null);
}

private enum IsSelectedVariant
{
SingleField,
MultipleFields,
FieldSet,
Pattern
}

public void Flush() => Writer.Flush();

private static string ToFullyQualifiedString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@ public static ResolverParameterKind GetParameterKind(
return ResolverParameterKind.ConnectionFlags;
}

if (parameter.IsIsSelected())
{
return ResolverParameterKind.IsSelected;
}

if (parameter.IsSelection())
{
return ResolverParameterKind.Selection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,19 @@ public static bool IsPagingArguments(this IParameterSymbol parameter)
public static bool IsSelection(this IParameterSymbol parameter)
=> parameter.Type.ToDisplayString() == WellKnownTypes.ISelection;

public static bool IsIsSelected(this IParameterSymbol parameter)
{
foreach (var attribute in parameter.GetAttributes())
{
if (attribute.AttributeClass?.ToDisplayString() == WellKnownAttributes.IsSelectedAttribute)
{
return true;
}
}

return false;
}

public static bool IsGlobalState(
this IParameterSymbol parameter,
Compilation compilation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ ResolverParameterKind.FieldNode or
ResolverParameterKind.OutputField or
ResolverParameterKind.ClaimsPrincipal or
ResolverParameterKind.ConnectionFlags or
ResolverParameterKind.Selection;
ResolverParameterKind.Selection or
ResolverParameterKind.IsSelected;

public bool RequiresBinding
=> Kind == ResolverParameterKind.Unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ public enum ResolverParameterKind
QueryContext,
PagingArguments,
ConnectionFlags,
Selection
Selection,
IsSelected
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static class WellKnownAttributes
public const string ObsoleteAttribute = "System.ObsoleteAttribute";
public const string BatchResolverAttribute = "HotChocolate.Types.BatchResolverAttribute";
public const string GraphQLTypeAttribute = "HotChocolate.GraphQLTypeAttribute";
public const string IsSelectedAttribute = "HotChocolate.Types.IsSelectedAttribute";

public static HashSet<string> BindAttributes { get; } =
[
Expand Down
Loading
Loading