Skip to content
Open
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 @@ -163,12 +163,29 @@ internal static void ResolveFieldSelection(
{
if (fieldSelection.IsConditional && !IsConditional(fieldSyntax))
{
// the previously selected field becomes a merged-away duplicate of the
// field we now promote to the canonical selection.
fieldSelection = new FieldSelection(
field,
fieldSyntax,
path.Append(responseName));
path.Append(responseName),
duplicates: AppendDuplicate(
fieldSelection.Duplicates,
fieldSelection.SyntaxNode));
fields[responseName] = fieldSelection;
}
else if (!ReferenceEquals(fieldSelection.SyntaxNode, fieldSyntax))
{
// the same response name is selected again, for example both directly and
// through a fragment. We keep the first selection canonical but remember the
// duplicate so its sub-selection set can still be resolved later.
fields[responseName] = new FieldSelection(
field,
fieldSelection.SyntaxNode,
fieldSelection.Path,
fieldSelection.IsConditional,
AppendDuplicate(fieldSelection.Duplicates, fieldSyntax));
}
}
else
{
Expand All @@ -190,6 +207,16 @@ internal static void ResolveFieldSelection(

private static bool IsConditional(IHasDirectives _) => false;

private static IReadOnlyList<FieldNode> AppendDuplicate(
IReadOnlyList<FieldNode> duplicates,
FieldNode duplicate)
{
var list = new List<FieldNode>(duplicates.Count + 1);
list.AddRange(duplicates);
list.Add(duplicate);
return list;
}

private void ResolveFragmentSpread(
FragmentSpreadNode fragmentSpreadSyntax,
IOutputTypeDefinition type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ public FieldSelection(
IOutputFieldDefinition field,
FieldNode syntaxNode,
Path path,
bool isConditional = true)
bool isConditional = true,
IReadOnlyList<FieldNode>? duplicates = null)
{
ResponseName = syntaxNode.Alias?.Value ?? syntaxNode.Name.Value;
Field = field;
SyntaxNode = syntaxNode;
IsConditional = isConditional;
Path = path;
Duplicates = duplicates ?? [];
}

public string ResponseName { get; }
Expand All @@ -29,6 +31,14 @@ public FieldSelection(

public bool IsConditional { get; }

/// <summary>
/// The other field syntax nodes that select the same response name at this level and were
/// merged into this selection. They carry the same field but a potentially different
/// sub-selection set, for example when a field is selected both directly and through a
/// fragment.
/// </summary>
public IReadOnlyList<FieldNode> Duplicates { get; }

public FieldSelection WithPath(Path path) =>
new(Field, SyntaxNode, path, IsConditional);
new(Field, SyntaxNode, path, IsConditional, Duplicates);
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ private OutputTypeModel AnalyzeWithDefaults(
selectionVariants.ReturnType.SyntaxNode,
returnType.SelectionSet);

RegisterDuplicateSelectionSets(context, fieldSelection, returnType);

foreach (var selectionSet in selectionVariants.Variants)
{
returnTypeFragment = FragmentHelper.CreateFragmentNode(
Expand Down Expand Up @@ -109,6 +111,28 @@ private OutputTypeModel AnalyzeWithDefaults(
return returnType;
}

// A composite field can be selected more than once at the same level, for example both
// directly and through a fragment. The selections are merged onto a single canonical field
// syntax node, but the parent type model may reference one of the merged-away duplicates as
// the property's syntax node. We register those duplicate sub-selection sets against the same
// result type so the property type can still be resolved later.
private static void RegisterDuplicateSelectionSets(
IDocumentAnalyzerContext context,
FieldSelection fieldSelection,
OutputTypeModel returnType)
{
foreach (var duplicate in fieldSelection.Duplicates)
{
if (duplicate.SelectionSet is { } selectionSet)
{
context.RegisterSelectionSet(
returnType.Type,
selectionSet,
returnType.SelectionSet);
}
}
}

private OutputTypeModel AnalyzeWithHoistedFragment(
IDocumentAnalyzerContext context,
FieldSelection fieldSelection,
Expand Down Expand Up @@ -136,6 +160,8 @@ private OutputTypeModel AnalyzeWithHoistedFragment(
selectionVariants.ReturnType.SyntaxNode,
returnType.SelectionSet);

RegisterDuplicateSelectionSets(context, fieldSelection, returnType);

foreach (var selectionSet in selectionVariants.Variants)
{
returnTypeFragment = FragmentHelper.CreateFragmentNode(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using static StrawberryShake.CodeGeneration.CSharp.GeneratorTestHelper;

namespace StrawberryShake.CodeGeneration.CSharp;

public class FragmentDirectAndSpreadGeneratorTests
{
private const string Schema =
"type Query { me: User } "
+ "type User { id: ID! name: String avatar: Image bestFriend: User } "
+ "type Image { url: String! width: Int height: Int }";

private const string KeyExtension = "extend schema @key(fields: \"id\")";

[Fact]
public void Generate_Should_Succeed_When_CompositeField_Selected_Direct_And_Via_InlineFragment()
{
// arrange
// avatar is selected both directly ({ url }) and inside an inline fragment ({ width }).

// act & assert
AssertResult(
"query Q { me { avatar { url } ... on User { avatar { width } } } }",
Schema,
KeyExtension);
}

[Fact]
public void Generate_Should_Succeed_When_CompositeField_Selected_Direct_And_Via_NamedFragment()
{
// arrange
// avatar is selected both directly ({ url }) and via a named fragment spread ({ width }).

// act & assert
AssertResult(
"query Q { me { avatar { url } ...UF } } fragment UF on User { avatar { width } }",
Schema,
KeyExtension);
}

[Fact]
public void Generate_Should_Succeed_When_SelfReferencingField_Selected_Direct_And_Via_InlineFragment()
{
// arrange
// bestFriend (a self-referencing User) is selected both directly and via an inline fragment.

// act & assert
AssertResult(
"query Q { me { bestFriend { id } ... on User { bestFriend { name } } } }",
Schema,
KeyExtension);
}

[Fact]
public void Generate_Should_Succeed_When_CompositeField_Selected_Direct_Twice()
{
// arrange
// control: avatar selected twice directly must keep working.

// act & assert
AssertResult(
"query Q { me { avatar { url } avatar { width } } }",
Schema,
KeyExtension);
}

[Fact]
public void Generate_Should_Succeed_When_CompositeField_Selected_In_Two_InlineFragments()
{
// arrange
// control: avatar selected only inside fragments must keep working.

// act & assert
AssertResult(
"query Q { me { ... on User { avatar { url } } ... on User { avatar { width } } } }",
Schema,
KeyExtension);
}
}
Loading
Loading