Skip to content

Commit 9a60aee

Browse files
authored
Fix false @defer/@stream label collision in reused fragments (#9709)
1 parent bb308a3 commit 9a60aee

3 files changed

Lines changed: 107 additions & 1 deletion

File tree

src/HotChocolate/Core/src/Validation/Rules/DirectiveVisitor.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,14 @@ private static void ValidateDirectives<T>(
172172
}
173173

174174
// Defer And Stream Directive Labels Are Unique
175+
// The spec iterates over every directive in the document. Because the
176+
// document walker descends into fragment definitions once per spread,
177+
// the same lexical @defer/@stream directive may be visited multiple
178+
// times. Track the processed directive nodes so each is counted once.
175179
if (node.Kind is Field or InlineFragment or FragmentSpread
176180
&& (directive.Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)
177-
|| directive.Name.Value.Equals(DirectiveNames.Stream.Name, StringComparison.Ordinal)))
181+
|| directive.Name.Value.Equals(DirectiveNames.Stream.Name, StringComparison.Ordinal))
182+
&& feature.ProcessedLabelDirectives.Add(directive))
178183
{
179184
switch (directive.GetArgumentValue(DirectiveNames.Defer.Arguments.Label))
180185
{
@@ -249,10 +254,13 @@ private sealed class DirectiveVisitorFeature : ValidatorFeature
249254

250255
public HashSet<string> Labels { get; } = [];
251256

257+
public HashSet<DirectiveNode> ProcessedLabelDirectives { get; } = [];
258+
252259
protected internal override void Reset()
253260
{
254261
DirectiveNames.Clear();
255262
Labels.Clear();
263+
ProcessedLabelDirectives.Clear();
256264
}
257265
}
258266
}

src/HotChocolate/Core/test/Validation.Tests/DeferAndStreamDirectiveLabelsAreUniqueTests.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,90 @@ ... @defer(label: $a) {
8787
t.Message));
8888
}
8989

90+
[Fact]
91+
public void Label_Should_Be_Valid_When_Defer_Is_In_Reused_Fragment()
92+
{
93+
ExpectValid(
94+
"""
95+
query {
96+
...Repeated
97+
...Repeated
98+
}
99+
100+
fragment Repeated on Query {
101+
... @defer(label: "details") {
102+
__typename
103+
}
104+
}
105+
""");
106+
}
107+
108+
[Fact]
109+
public void Label_Should_Be_Valid_When_Stream_Is_In_Reused_Fragment()
110+
{
111+
ExpectValid(
112+
"""
113+
query {
114+
...Repeated
115+
...Repeated
116+
}
117+
118+
fragment Repeated on Query {
119+
__schema {
120+
_types @stream(label: "types") {
121+
name
122+
}
123+
}
124+
}
125+
""");
126+
}
127+
128+
[Fact]
129+
public void Label_Should_Be_Valid_When_Fragment_Is_Spread_Across_Operations()
130+
{
131+
ExpectValid(
132+
"""
133+
query A {
134+
...Repeated
135+
}
136+
137+
query B {
138+
...Repeated
139+
}
140+
141+
fragment Repeated on Query {
142+
... @defer(label: "details") {
143+
__typename
144+
}
145+
}
146+
""");
147+
}
148+
149+
[Fact]
150+
public void Label_Duplicate_When_Reused_Fragment_Label_Also_Used_Outside()
151+
{
152+
ExpectErrors(
153+
"""
154+
query {
155+
...Repeated
156+
...Repeated
157+
... @defer(label: "details") {
158+
__typename
159+
}
160+
}
161+
162+
fragment Repeated on Query {
163+
... @defer(label: "details") {
164+
__typename
165+
}
166+
}
167+
""",
168+
t => Assert.Equal(
169+
"If a label is passed, it must be unique within all other @defer "
170+
+ "and @stream directives in the document.",
171+
t.Message));
172+
}
173+
90174
[Fact]
91175
public void Label_Can_Be_Null_And_Is_Optional_And_Can_Be_A_Unique_Name()
92176
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"message": "If a label is passed, it must be unique within all other @defer and @stream directives in the document.",
3+
"locations": [
4+
{
5+
"line": 4,
6+
"column": 3
7+
}
8+
],
9+
"extensions": {
10+
"specifiedBy": "https://spec.graphql.org/draft/#sec-Defer-And-Stream-Directive-Labels-Are-Unique",
11+
"rfc": "https://github.com/graphql/graphql-spec/pull/1110",
12+
"label": "details"
13+
}
14+
}

0 commit comments

Comments
 (0)