fix(strawberryshake): resolve output type for composite field selected directly and via fragment#9
Open
cliedeman wants to merge 2 commits into
Open
Conversation
…d directly and via fragment A composite (object/interface) field selected BOTH directly AND via a fragment (an inline fragment of the same type OR a named fragment spread) at the same selection level made CSharpGenerator.Generate throw: System.InvalidOperationException: Could not find an output type for the specified field syntax. Root cause: the field selections are merged onto a single canonical FieldNode, but two code paths disagree on which node is canonical. FieldCollector merges in document order (direct selection wins), while FragmentHelper builds the parent type model's fields by processing inline fragments first (fragment selection wins). The parent property therefore referenced a merged-away duplicate node, whose sub-selection set was never registered in OperationModel's selection-set lookup, so TypeDescriptorMapper.GetFieldTypeDescriptor could not resolve the field's result type and threw. Fix: track the merged-away duplicate field nodes on FieldSelection and register their sub-selection sets against the same result type in InterfaceTypeSelectionSetAnalyzer, so the property type resolves regardless of which equivalent node the parent model captured. This makes the mixed direct+fragment case behave identically to the already-working homogeneous cases (both direct, both in fragments). Adds FragmentDirectAndSpreadGeneratorTests covering the three repros plus the two control cases.
10 tasks
cliedeman
force-pushed
the
fix/ss-fragment-direct-and-spread-output-type
branch
from
June 27, 2026 14:34
c41609a to
68caf34
Compare
Patch coverage84.2% of changed lines covered (32/38)
Uncovered changed lines (JSON){
"sha": "7d0dc2e5c2b492248c71362456156003c5c99a71",
"files": [
{ "path": "src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/FieldCollector.cs", "ranges": [[171, 174]] },
{ "path": "src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/FieldSelection.cs", "ranges": [[43, 43]] },
{ "path": "src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/InterfaceTypeSelectionSetAnalyzer.cs", "ranges": [[163, 163]] }
]
}Project coverage: 52.4% (215319/411064 lines) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
A composite (object/interface) field selected both directly and via a fragment (an inline fragment of the same type, or a named fragment spread) at the same selection level makes
CSharpGenerator.Generatethrow:There is no upstream issue for this; it is described here.
Minimal repros (all valid GraphQL, all threw before this change)
Cases that already worked and still work (regression guards)
Root cause
When the same composite field is selected more than once at a level, the selections are merged onto a single canonical
FieldNode. Two code paths disagree on which node is canonical:FieldCollector.ResolveFieldSelectionmerges in document order, so the direct selection (avatar { url }) wins and becomesFieldSelection.SyntaxNode. This is the node used to descend into the child field and to callIDocumentAnalyzerContext.RegisterSelectionSet, so the selection-set lookup is keyed on(Image, { url }).FragmentHelper.CollectFieldsbuilds the parent type model's fields by processing inline-fragment nodes before top-level fields, so the fragment selection (avatar { width }) wins and becomes the parent property'sOutputFieldModel.SyntaxNode.Later,
TypeDescriptorMapper.GetFieldTypeDescriptorresolves the parent property's type viaOperationModel.TryGetFieldResultType, which looks up_selectionSets[(fieldNamedType, fieldSyntax.SelectionSet)]and then matches an interfaceOutputTypeModelby reference-equality on the selection-set node (OperationModel.cs,TryGetFieldResultType). The lookup key is(Image, { width }), but only(Image, { url })was registered, so the lookup misses, the fallback node matches no interface output type, andGetFieldTypeDescriptorthrows.The two control cases work precisely because both selections come from the same kind of node (both direct, or both fragments), so both merge orders pick the same canonical node and the lookup key matches.
The fix
Track the merged-away duplicate field nodes on
FieldSelection(Duplicates), populate them inFieldCollector.ResolveFieldSelection, and inInterfaceTypeSelectionSetAnalyzerregister each duplicate's sub-selection set against the same result type. The property type then resolves regardless of which equivalent node the parent model captured.This makes the mixed direct+fragment case behave identically to the already-working homogeneous cases (the generated
IQ_Me_Avatarinterface is the same shape it produces forboth directandboth in fragments). Deep-merging the duplicate sub-selections into one interface is a separate, pre-existing analyzer characteristic and is intentionally out of scope.Files changed (production)
src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/FieldSelection.cs— addsDuplicates.src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/FieldCollector.cs— records merged-away duplicates.src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/InterfaceTypeSelectionSetAnalyzer.cs— registers duplicate sub-selection sets.Tests
Adds
FragmentDirectAndSpreadGeneratorTests(new, isolated class) covering the three repros plus the two control cases.AssertResultruns the generator (so a throw fails the test), snapshots the output, and Roslyn-compiles it.Verification
InvalidOperationException: Could not find an output type for the specified field syntax.atTypeDescriptorMapper.GetFieldTypeDescriptor.StrawberryShake.CodeGeneration.CSharp.Testssuite: 194 passed, 1 pre-existing skip, 0 failed. No existing snapshot changed; only the 5 new snapshots were added.— Claude
🤖 Generated with Claude Code