Skip to content

Commit a4d66f5

Browse files
committed
backport: Introduce "recommended" validation rules
Replicates graphql/graphql-js@c985c27
1 parent c25eee4 commit a4d66f5

File tree

8 files changed

+608
-2
lines changed

8 files changed

+608
-2
lines changed

docs/modules/validation.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ Rules
137137

138138
.. autoclass:: VariablesInAllowedPositionRule
139139

140+
**No spec section: "Maximum introspection depth"**
141+
142+
.. autoclass:: MaxIntrospectionDepthRule
143+
140144
**SDL-specific validation rules**
141145

142146
.. autoclass:: LoneSchemaDefinitionRule

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ addopts = --benchmark-disable
1313
python_classes = PyTest*
1414
# Handle all async fixtures and tests automatically by asyncio
1515
asyncio_mode = auto
16+
# Default event loop scope used for asynchronous fixtures
17+
asyncio_default_fixture_loop_scope = function
1618
# Set a timeout in seconds for aborting tests that run too long.
1719
timeout = 100
1820
# Ignore config options not (yet) available in older Python versions.

src/graphql/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@
378378
SDLValidationRule,
379379
# All validation rules in the GraphQL Specification.
380380
specified_rules,
381+
recommended_rules,
381382
# Individual validation rules.
382383
ExecutableDefinitionsRule,
383384
FieldsOnCorrectTypeRule,
@@ -417,6 +418,8 @@
417418
# Custom validation rules
418419
NoDeprecatedCustomRule,
419420
NoSchemaIntrospectionCustomRule,
421+
# Recommended validation rules
422+
MaxIntrospectionDepthRule,
420423
)
421424

422425
# Execute GraphQL documents.
@@ -700,6 +703,7 @@
700703
"ASTValidationRule",
701704
"SDLValidationRule",
702705
"specified_rules",
706+
"recommended_rules",
703707
"ExecutableDefinitionsRule",
704708
"FieldsOnCorrectTypeRule",
705709
"FragmentsOnCompositeTypesRule",
@@ -736,6 +740,7 @@
736740
"PossibleTypeExtensionsRule",
737741
"NoDeprecatedCustomRule",
738742
"NoSchemaIntrospectionCustomRule",
743+
"MaxIntrospectionDepthRule",
739744
"GraphQLError",
740745
"GraphQLErrorExtensions",
741746
"GraphQLFormattedError",

src/graphql/validation/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .rules import ValidationRule, ASTValidationRule, SDLValidationRule
1616

1717
# All validation rules in the GraphQL Specification.
18-
from .specified_rules import specified_rules
18+
from .specified_rules import specified_rules, recommended_rules
1919

2020
# Spec Section: "Executable Definitions"
2121
from .rules.executable_definitions import ExecutableDefinitionsRule
@@ -95,6 +95,9 @@
9595
# Spec Section: "All Variable Usages Are Allowed"
9696
from .rules.variables_in_allowed_position import VariablesInAllowedPositionRule
9797

98+
# No spec section: "Maximum introspection depth"
99+
from .rules.max_introspection_depth_rule import MaxIntrospectionDepthRule
100+
98101
# SDL-specific validation rules
99102
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
100103
from .rules.unique_operation_types import UniqueOperationTypesRule
@@ -118,6 +121,7 @@
118121
"ValidationContext",
119122
"ValidationRule",
120123
"specified_rules",
124+
"recommended_rules",
121125
"ExecutableDefinitionsRule",
122126
"FieldsOnCorrectTypeRule",
123127
"FragmentsOnCompositeTypesRule",
@@ -126,6 +130,7 @@
126130
"KnownFragmentNamesRule",
127131
"KnownTypeNamesRule",
128132
"LoneAnonymousOperationRule",
133+
"MaxIntrospectionDepthRule",
129134
"NoFragmentCyclesRule",
130135
"NoUndefinedVariablesRule",
131136
"NoUnusedFragmentsRule",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Max introspection depth rule"""
2+
3+
from typing import Dict, Any
4+
5+
from ...error import GraphQLError
6+
from ...language import SKIP, FieldNode, FragmentSpreadNode, Node, VisitorAction
7+
from . import ASTValidationRule, ValidationContext
8+
9+
__all__ = ["MaxIntrospectionDepthRule"]
10+
11+
MAX_LIST_DEPTH = 3
12+
13+
14+
class MaxIntrospectionDepthRule(ASTValidationRule):
15+
"""Checks maximum introspection depth"""
16+
17+
def __init__(self, context: ValidationContext) -> None:
18+
super().__init__(context)
19+
self._visited_fragments: Dict[str, None] = {}
20+
self._get_fragment = context.get_fragment
21+
22+
def _check_depth(self, node: Node, depth: int = 0) -> bool:
23+
"""Check whether the maximum introspection depth has been reached.
24+
25+
Counts the depth of list fields in "__Type" recursively
26+
and returns `True` if the limit has been reached.
27+
"""
28+
if isinstance(node, FragmentSpreadNode):
29+
visited_fragments = self._visited_fragments
30+
fragment_name = node.name.value
31+
if fragment_name in visited_fragments:
32+
# Fragment cycles are handled by `NoFragmentCyclesRule`.
33+
return False
34+
fragment = self._get_fragment(fragment_name)
35+
if not fragment:
36+
# Missing fragments checks are handled by the `KnownFragmentNamesRule`.
37+
return False
38+
39+
# Rather than following an immutable programming pattern which has
40+
# significant memory and garbage collection overhead, we've opted to take
41+
# a mutable approach for efficiency's sake. Importantly visiting a fragment
42+
# twice is fine, so long as you don't do one visit inside the other.
43+
visited_fragments[fragment_name] = None
44+
try:
45+
return self._check_depth(fragment, depth)
46+
finally:
47+
del visited_fragments[fragment_name]
48+
49+
if isinstance(node, FieldNode) and node.name.value in (
50+
# check all introspection lists
51+
"fields",
52+
"interfaces",
53+
"possibleTypes",
54+
"inputFields",
55+
):
56+
depth += 1
57+
if depth >= MAX_LIST_DEPTH:
58+
return True
59+
60+
# hendle fields and inline fragments
61+
try:
62+
selection_set = node.selection_set # type: ignore[attr-defined]
63+
except AttributeError: # pragma: no cover
64+
selection_set = None
65+
if selection_set:
66+
for child in selection_set.selections:
67+
if self._check_depth(child, depth):
68+
return True
69+
70+
return False
71+
72+
def enter_field(self, node: FieldNode, *_args: Any) -> VisitorAction:
73+
if node.name.value in ("__schema", "__type") and self._check_depth(node):
74+
self.report_error(
75+
GraphQLError(
76+
"Maximum introspection depth exceeded",
77+
[node],
78+
)
79+
)
80+
return SKIP
81+
return None

src/graphql/validation/specified_rules.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
# Spec Section: "Input Object Field Uniqueness"
8383
from .rules.unique_input_field_names import UniqueInputFieldNamesRule
8484

85+
# No spec section: "Maximum introspection depth"
86+
from .rules.max_introspection_depth_rule import MaxIntrospectionDepthRule
87+
8588
# Schema definition language:
8689
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
8790
from .rules.unique_operation_types import UniqueOperationTypesRule
@@ -92,7 +95,13 @@
9295
from .rules.unique_directive_names import UniqueDirectiveNamesRule
9396
from .rules.possible_type_extensions import PossibleTypeExtensionsRule
9497

95-
__all__ = ["specified_rules", "specified_sdl_rules"]
98+
__all__ = ["specified_rules", "specified_sdl_rules", "recommended_rules"]
99+
100+
# Technically these aren't part of the spec but they are strongly encouraged
101+
# validation rules.
102+
103+
recommended_rules: Tuple[Type[ASTValidationRule], ...] = (MaxIntrospectionDepthRule,)
104+
"""A tuple with all recommended validation rules."""
96105

97106

98107
# This list includes all validation rules defined by the GraphQL spec.
@@ -127,6 +136,7 @@
127136
VariablesInAllowedPositionRule,
128137
OverlappingFieldsCanBeMergedRule,
129138
UniqueInputFieldNamesRule,
139+
*recommended_rules,
130140
)
131141
"""A tuple with all validation rules defined by the GraphQL specification.
132142

tests/execution/test_executor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,7 @@ def resolves_to_an_error_if_schema_does_not_support_operation():
883883
)
884884

885885
@mark.asyncio
886+
@mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning")
886887
async def correct_field_ordering_despite_execution_order():
887888
schema = GraphQLSchema(
888889
GraphQLObjectType(

0 commit comments

Comments
 (0)