[Analyzer] Support IsSelected attribute#9527
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the HotChocolate Types.Analyzers source generator to recognize resolver parameters annotated with [IsSelected] and generate optimized selection-check code (single field, multiple fields, and selection patterns), with accompanying snapshot and integration test coverage.
Changes:
- Add
ResolverParameterKind.IsSelectedand detection viaIsSelectedAttribute. - Generate resolver argument binding code for
[IsSelected], including precomputed field sets / parsed selection sets when beneficial. - Add analyzer snapshot tests and new integration tests validating runtime behavior (including node resolvers) and update schema snapshot.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs | Adds missing metadata references needed to compile generated code that uses parser/visitor types. |
| src/HotChocolate/Core/test/Types.Analyzers.Tests/ResolverTests.cs | Adds snapshot tests for [IsSelected] scenarios (single/multi/pattern, node resolver). |
| src/HotChocolate/Core/test/Types.Analyzers.Tests/snapshots/ResolverTests.GenerateSource_ResolverWithIsSelectedSingleField_MatchesSnapshot.md | New golden snapshot for single-field [IsSelected]. |
| src/HotChocolate/Core/test/Types.Analyzers.Tests/snapshots/ResolverTests.GenerateSource_ResolverWithIsSelectedMultipleFields_MatchesSnapshot.md | New golden snapshot for multi-field [IsSelected] (field-set optimization). |
| src/HotChocolate/Core/test/Types.Analyzers.Tests/snapshots/ResolverTests.GenerateSource_ResolverWithIsSelectedPattern_MatchesSnapshot.md | New golden snapshot for pattern-based [IsSelected] (parsed selection set + visitor). |
| src/HotChocolate/Core/test/Types.Analyzers.Tests/snapshots/ResolverTests.GenerateSource_NodeResolverWithIsSelectedPattern_MatchesSnapshot.md | New golden snapshot covering node resolver + pattern-based [IsSelected]. |
| src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Product.cs | Adds query + node type used to validate [IsSelected] at runtime. |
| src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/IsSelectedTests.cs | Adds integration tests validating true/false outcomes for field selection and node resolution. |
| src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/snapshots/IntegrationTests.Schema_Snapshot.snap | Updates schema snapshot to include the new isSelectedTest field and IsSelectedNode type. |
| src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs | Adds IsSelectedAttribute to well-known attribute constants. |
| src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs | Introduces IsSelected parameter kind. |
| src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs | Treats IsSelected as “pure” (no binding required). |
| src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs | Adds attribute-based detection for [IsSelected]. |
| src/HotChocolate/Core/src/Types.Analyzers/Helpers/CompilationExtensions.cs | Maps [IsSelected] parameters to the new ResolverParameterKind.IsSelected. |
| src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs | Core generator updates: emits argument generation + optional precomputed fields/selection parsing for [IsSelected]. |
| src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs | Ensures node resolver paths also emit/init [IsSelected] helpers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| Writer.WriteIndentedLine( | ||
| "_isSelected_{0}_{1} = new global::System.Collections.Generic.HashSet<string>([{2}]);", |
There was a problem hiding this comment.
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.
| "_isSelected_{0}_{1} = new global::System.Collections.Generic.HashSet<string>([{2}]);", | |
| "_isSelected_{0}_{1} = new global::System.Collections.Generic.HashSet<string>(new string[] {{ {2} }});", |
| Writer.WriteIndentedLine( | ||
| $"_isSelected_{resolver.Member.Name}_{parameter.Name} = global::HotChocolate.Language.Utf8GraphQLParser.Syntax.ParseSelectionSet(\"{{ {patternString} }}\");"); |
There was a problem hiding this comment.
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).
| 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}\");"); |
| // 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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]).
No description provided.