Skip to content

Commit 5ea3d40

Browse files
committed
Introduce "recommended" validation rules
Replicates graphql/graphql-js@2744f58
1 parent 9896b84 commit 5ea3d40

File tree

5 files changed

+599
-2
lines changed

5 files changed

+599
-2
lines changed

src/graphql/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@
385385
SDLValidationRule,
386386
# All validation rules in the GraphQL Specification.
387387
specified_rules,
388+
recommended_rules,
388389
# Individual validation rules.
389390
ExecutableDefinitionsRule,
390391
FieldsOnCorrectTypeRule,
@@ -412,6 +413,7 @@
412413
ValuesOfCorrectTypeRule,
413414
VariablesAreInputTypesRule,
414415
VariablesInAllowedPositionRule,
416+
MaxIntrospectionDepthRule,
415417
# SDL-specific validation rules
416418
LoneSchemaDefinitionRule,
417419
UniqueOperationTypesRule,
@@ -613,6 +615,7 @@
613615
"Location",
614616
"LoneAnonymousOperationRule",
615617
"LoneSchemaDefinitionRule",
618+
"MaxIntrospectionDepthRule",
616619
"Middleware",
617620
"MiddlewareManager",
618621
"NameNode",
@@ -798,6 +801,7 @@
798801
"print_schema",
799802
"print_source_location",
800803
"print_type",
804+
"recommended_rules",
801805
"resolve_thunk",
802806
"separate_operations",
803807
"specified_directives",

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: "Defer And Stream Directive Labels Are Unique"
2121
from .rules.defer_stream_directive_label import DeferStreamDirectiveLabel
@@ -109,6 +109,9 @@
109109
# Spec Section: "All Variable Usages Are Allowed"
110110
from .rules.variables_in_allowed_position import VariablesInAllowedPositionRule
111111

112+
# No spec section: "Maximum introspection depth"
113+
from .rules.max_introspection_depth_rule import MaxIntrospectionDepthRule
114+
112115
# SDL-specific validation rules
113116
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
114117
from .rules.unique_operation_types import UniqueOperationTypesRule
@@ -138,6 +141,7 @@
138141
"KnownTypeNamesRule",
139142
"LoneAnonymousOperationRule",
140143
"LoneSchemaDefinitionRule",
144+
"MaxIntrospectionDepthRule",
141145
"NoDeprecatedCustomRule",
142146
"NoFragmentCyclesRule",
143147
"NoSchemaIntrospectionCustomRule",
@@ -170,6 +174,7 @@
170174
"ValuesOfCorrectTypeRule",
171175
"VariablesAreInputTypesRule",
172176
"VariablesInAllowedPositionRule",
177+
"recommended_rules",
173178
"specified_rules",
174179
"validate",
175180
]
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 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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
# Schema definition language:
4646
from .rules.lone_schema_definition import LoneSchemaDefinitionRule
4747

48+
# No spec section: "Maximum introspection depth"
49+
from .rules.max_introspection_depth_rule import MaxIntrospectionDepthRule
50+
4851
# Spec Section: "Fragments must not form cycles"
4952
from .rules.no_fragment_cycles import NoFragmentCyclesRule
5053

@@ -115,8 +118,13 @@
115118
if TYPE_CHECKING:
116119
from .rules import ASTValidationRule
117120

118-
__all__ = ["specified_rules", "specified_sdl_rules"]
121+
__all__ = ["recommended_rules", "specified_rules", "specified_sdl_rules"]
122+
123+
124+
# Technically these aren't part of the spec but they are strongly encouraged
125+
# validation rules.
119126

127+
recommended_rules: tuple[type[ASTValidationRule], ...] = (MaxIntrospectionDepthRule,)
120128

121129
# This list includes all validation rules defined by the GraphQL spec.
122130
#
@@ -154,6 +162,7 @@
154162
VariablesInAllowedPositionRule,
155163
OverlappingFieldsCanBeMergedRule,
156164
UniqueInputFieldNamesRule,
165+
*recommended_rules,
157166
)
158167
"""A tuple with all validation rules defined by the GraphQL specification.
159168

0 commit comments

Comments
 (0)