Skip to content

Commit 6765215

Browse files
committed
OverlappingFieldsCanBeMerged: sort argument values before comparing
Replicates graphql/graphql-js@40c160e
1 parent a63cae9 commit 6765215

File tree

5 files changed

+126
-23
lines changed

5 files changed

+126
-23
lines changed

src/graphql/utilities/find_breaking_changes.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from enum import Enum
22
from typing import Any, Collection, Dict, List, NamedTuple, Union, cast
33

4-
from ..language import print_ast, visit, ObjectValueNode, Visitor
5-
from ..pyutils import inspect, natural_comparison_key, Undefined
4+
from ..language import print_ast
5+
from ..pyutils import inspect, Undefined
66
from ..type import (
77
GraphQLEnumType,
88
GraphQLField,
@@ -28,6 +28,7 @@
2828
is_specified_scalar_type,
2929
is_union_type,
3030
)
31+
from ..utilities.sort_value_node import sort_value_node
3132
from .ast_from_value import ast_from_value
3233

3334
__all__ = [
@@ -555,24 +556,7 @@ def stringify_value(value: Any, type_: GraphQLInputType) -> str:
555556
ast = ast_from_value(value, type_)
556557
if ast is None: # pragma: no cover
557558
raise TypeError(f"Invalid value: {inspect(value)}")
558-
559-
class SortVisitor(Visitor):
560-
@staticmethod
561-
def enter_object_value(
562-
object_value_node: ObjectValueNode, *_args: Any
563-
) -> ObjectValueNode:
564-
object_value_node.fields = tuple(
565-
sorted(
566-
object_value_node.fields,
567-
key=lambda node: natural_comparison_key(node.name.value),
568-
)
569-
)
570-
571-
return object_value_node
572-
573-
sorted_ast = visit(ast, SortVisitor())
574-
575-
return print_ast(sorted_ast)
559+
return print_ast(sort_value_node(ast))
576560

577561

578562
class ListDiff(NamedTuple):
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from copy import copy
2+
from typing import Tuple
3+
4+
from ..language import ListValueNode, ObjectFieldNode, ObjectValueNode, ValueNode
5+
from ..pyutils import natural_comparison_key
6+
7+
__all__ = ["sort_value_node"]
8+
9+
10+
def sort_value_node(value_node: ValueNode) -> ValueNode:
11+
"""Sort ValueNode.
12+
13+
This function returns a sorted copy of the given ValueNode
14+
15+
For internal use only.
16+
"""
17+
if isinstance(value_node, ObjectValueNode):
18+
value_node = copy(value_node)
19+
value_node.fields = sort_fields(value_node.fields)
20+
elif isinstance(value_node, ListValueNode):
21+
value_node = copy(value_node)
22+
value_node.values = tuple(sort_value_node(value) for value in value_node.values)
23+
return value_node
24+
25+
26+
def sort_field(field: ObjectFieldNode) -> ObjectFieldNode:
27+
field = copy(field)
28+
field.value = sort_value_node(field.value)
29+
return field
30+
31+
32+
def sort_fields(fields: Tuple[ObjectFieldNode, ...]) -> Tuple[ObjectFieldNode, ...]:
33+
return tuple(
34+
sorted(
35+
(sort_field(field) for field in fields),
36+
key=lambda field: natural_comparison_key(field.name.value),
37+
)
38+
)

src/graphql/validation/rules/overlapping_fields_can_be_merged.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
is_object_type,
2828
)
2929
from ...utilities import type_from_ast
30+
from ...utilities.sort_value_node import sort_value_node
3031
from . import ValidationContext, ValidationRule
3132

3233
MYPY = False
@@ -589,16 +590,16 @@ def same_arguments(
589590
for argument1 in arguments1:
590591
for argument2 in arguments2:
591592
if argument2.name.value == argument1.name.value:
592-
if not same_value(argument1.value, argument2.value):
593+
if stringify_value(argument1.value) != stringify_value(argument2.value):
593594
return False
594595
break
595596
else:
596597
return False
597598
return True
598599

599600

600-
def same_value(value1: ValueNode, value2: ValueNode) -> bool:
601-
return print_ast(value1) == print_ast(value2)
601+
def stringify_value(value: ValueNode) -> str:
602+
return print_ast(sort_value_node(value))
602603

603604

604605
def do_types_conflict(type1: GraphQLOutputType, type2: GraphQLOutputType) -> bool:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from graphql.language import parse_value, print_ast
2+
from graphql.utilities.sort_value_node import sort_value_node
3+
4+
5+
def describe_sort_value_node():
6+
def _expect_sorted_value(source: str, expected: str) -> None:
7+
assert print_ast(sort_value_node(parse_value(source))) == expected
8+
9+
def do_not_change_non_object_values():
10+
_expect_sorted_value("1", "1")
11+
_expect_sorted_value("3.14", "3.14")
12+
_expect_sorted_value("null", "null")
13+
_expect_sorted_value("true", "true")
14+
_expect_sorted_value("false", "false")
15+
_expect_sorted_value('"cba"', '"cba"')
16+
_expect_sorted_value(
17+
'[1, 3.14, null, false, "cba"]', '[1, 3.14, null, false, "cba"]'
18+
)
19+
_expect_sorted_value(
20+
'[[1, 3.14, null, false, "cba"]]', '[[1, 3.14, null, false, "cba"]]'
21+
)
22+
23+
def sort_input_object_fields():
24+
_expect_sorted_value("{ b: 2, a: 1 }", "{a: 1, b: 2}")
25+
_expect_sorted_value("{ a: { c: 3, b: 2 } }", "{a: {b: 2, c: 3}}")
26+
_expect_sorted_value(
27+
"[{ b: 2, a: 1 }, { d: 4, c: 3}]",
28+
"[{a: 1, b: 2}, {c: 3, d: 4}]",
29+
)
30+
_expect_sorted_value(
31+
"{ b: { g: 7, f: 6 }, c: 3 , a: { d: 4, e: 5 } }",
32+
"{a: {d: 4, e: 5}, b: {f: 6, g: 7}, c: 3}",
33+
)

tests/validation/test_overlapping_fields_can_be_merged.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,53 @@ def allows_different_args_where_no_conflict_is_possible():
229229
"""
230230
)
231231

232+
def allows_different_order_of_args():
233+
schema = build_schema(
234+
"""
235+
type Query {
236+
someField(a: String, b: String): String
237+
}
238+
"""
239+
)
240+
# This is valid since arguments are unordered, see:
241+
# https://spec.graphql.org/draft/#
242+
# sec-Language.Arguments.Arguments-are-unordered
243+
assert_valid(
244+
"""
245+
{
246+
someField(a: null, b: null)
247+
someField(b: null, a: null)
248+
}
249+
""",
250+
schema=schema,
251+
)
252+
253+
def allows_different_order_of_input_object_fields_in_arg_values():
254+
schema = build_schema(
255+
"""
256+
input SomeInput {
257+
a: String
258+
b: String
259+
}
260+
261+
type Query {
262+
someField(arg: SomeInput): String
263+
}
264+
"""
265+
)
266+
# This is valid since input object fields are unordered, see:
267+
# https://spec.graphql.org/draft/#
268+
# sec-Input-Object-Values.Input-object-fields-are-unordered
269+
assert_valid(
270+
"""
271+
{
272+
someField(arg: { a: null, b: null })
273+
someField(arg: { b: null, a: null })
274+
}
275+
""",
276+
schema=schema,
277+
)
278+
232279
def encounters_conflict_in_fragments():
233280
assert_errors(
234281
"""

0 commit comments

Comments
 (0)