84
84
def parse (
85
85
source : SourceType ,
86
86
no_location : bool = False ,
87
+ max_tokens : Optional [int ] = None ,
87
88
allow_legacy_fragment_variables : bool = False ,
88
89
experimental_client_controlled_nullability : bool = False ,
89
90
) -> DocumentNode :
@@ -95,6 +96,12 @@ def parse(
95
96
they correspond to. The ``no_location`` option disables that behavior for
96
97
performance or testing.
97
98
99
+ Parser CPU and memory usage is linear to the number of tokens in a document,
100
+ however in extreme cases it becomes quadratic due to memory exhaustion.
101
+ Parsing happens before validation so even invalid queries can burn lots of
102
+ CPU time and memory.
103
+ To prevent this you can set a maximum number of tokens allowed within a document.
104
+
98
105
Legacy feature (will be removed in v3.3):
99
106
100
107
If ``allow_legacy_fragment_variables`` is set to ``True``, the parser will
@@ -131,6 +138,7 @@ def parse(
131
138
parser = Parser (
132
139
source ,
133
140
no_location = no_location ,
141
+ max_tokens = max_tokens ,
134
142
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
135
143
experimental_client_controlled_nullability = experimental_client_controlled_nullability , # noqa
136
144
)
@@ -140,6 +148,7 @@ def parse(
140
148
def parse_value (
141
149
source : SourceType ,
142
150
no_location : bool = False ,
151
+ max_tokens : Optional [int ] = None ,
143
152
allow_legacy_fragment_variables : bool = False ,
144
153
) -> ValueNode :
145
154
"""Parse the AST for a given string containing a GraphQL value.
@@ -155,6 +164,7 @@ def parse_value(
155
164
parser = Parser (
156
165
source ,
157
166
no_location = no_location ,
167
+ max_tokens = max_tokens ,
158
168
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
159
169
)
160
170
parser .expect_token (TokenKind .SOF )
@@ -166,6 +176,7 @@ def parse_value(
166
176
def parse_const_value (
167
177
source : SourceType ,
168
178
no_location : bool = False ,
179
+ max_tokens : Optional [int ] = None ,
169
180
allow_legacy_fragment_variables : bool = False ,
170
181
) -> ConstValueNode :
171
182
"""Parse the AST for a given string containing a GraphQL constant value.
@@ -176,6 +187,7 @@ def parse_const_value(
176
187
parser = Parser (
177
188
source ,
178
189
no_location = no_location ,
190
+ max_tokens = max_tokens ,
179
191
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
180
192
)
181
193
parser .expect_token (TokenKind .SOF )
@@ -187,6 +199,7 @@ def parse_const_value(
187
199
def parse_type (
188
200
source : SourceType ,
189
201
no_location : bool = False ,
202
+ max_tokens : Optional [int ] = None ,
190
203
allow_legacy_fragment_variables : bool = False ,
191
204
) -> TypeNode :
192
205
"""Parse the AST for a given string containing a GraphQL Type.
@@ -202,6 +215,7 @@ def parse_type(
202
215
parser = Parser (
203
216
source ,
204
217
no_location = no_location ,
218
+ max_tokens = max_tokens ,
205
219
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
206
220
)
207
221
parser .expect_token (TokenKind .SOF )
@@ -222,27 +236,32 @@ class Parser:
222
236
library, please use the `__version_info__` variable for version detection.
223
237
"""
224
238
225
- _lexer : Lexer
226
239
_no_location : bool
240
+ _max_tokens : Optional [int ]
227
241
_allow_legacy_fragment_variables : bool
228
242
_experimental_client_controlled_nullability : bool
243
+ _lexer : Lexer
244
+ _token_counter : int
229
245
230
246
def __init__ (
231
247
self ,
232
248
source : SourceType ,
233
249
no_location : bool = False ,
250
+ max_tokens : Optional [int ] = None ,
234
251
allow_legacy_fragment_variables : bool = False ,
235
252
experimental_client_controlled_nullability : bool = False ,
236
253
):
237
254
if not is_source (source ):
238
255
source = Source (cast (str , source ))
239
256
240
- self ._lexer = Lexer (source )
241
257
self ._no_location = no_location
258
+ self ._max_tokens = max_tokens
242
259
self ._allow_legacy_fragment_variables = allow_legacy_fragment_variables
243
260
self ._experimental_client_controlled_nullability = (
244
261
experimental_client_controlled_nullability
245
262
)
263
+ self ._lexer = Lexer (source )
264
+ self ._token_counter = 0
246
265
<
9E88
div class="diff-text-inner">
247
266
def parse_name (self ) -> NameNode :
248
267
"""Convert a name lex token into a name parse node."""
@@ -546,7 +565,7 @@ def parse_value_literal(self, is_const: bool) -> ValueNode:
546
565
547
566
def parse_string_literal (self , _is_const : bool = False ) -> StringValueNode :
548
567
token = self ._lexer .token
549
- self ._lexer . advance ()
568
+ self .advance_lexer ()
550
569
return StringValueNode (
551
570
value = token .value ,
552
571
block = token .kind == TokenKind .BLOCK_STRING ,
@@ -583,18 +602,18 @@ def parse_object(self, is_const: bool) -> ObjectValueNode:
583
602
584
603
def parse_int (self , _is_const : bool = False ) -> IntValueNode :
585
604
token = self ._lexer .token
586
- self ._lexer . advance ()
605
+ self .advance_lexer ()
587
606
return IntValueNode (value = token .value , loc = self .loc (token ))
588
607
589
608
def parse_float (self , _is_const : bool = False ) -> FloatValueNode :
590
609
token = self ._lexer .token
591
- self ._lexer . advance ()
610
+ self .advance_lexer ()
592
611
return FloatValueNode (value = token .value , loc = self .loc (token ))
593
612
594
613
def parse_named_values (self , _is_const : bool = False ) -> ValueNode :
595
614
token = self ._lexer .token
596
615
value = token .value
597
- self ._lexer . advance ()
616
+ self .advance_lexer ()
598
617
if value == "true" :
599
618
return BooleanValueNode (value = True , loc = self .loc (token ))
600
619
if value == "false" :
@@ -1089,7 +1108,7 @@ def expect_token(self, kind: TokenKind) -> Token:
1089
1108
"""
1090
1109
token = self ._lexer .token
1091
1110
if token .kind == kind :
1092
- self ._lexer . advance ()
1111
+ self .advance_lexer ()
1093
1112
return token
1094
1113
1095
1114
raise GraphQLSyntaxError (
@@ -1106,7 +1125,7 @@ def expect_optional_token(self, kind: TokenKind) -> bool:
1106
1125
"""
1107
1126
token = self ._lexer .token
1108
1127
if token .kind == kind :
1109
- self ._lexer . advance ()
1128
+ self .advance_lexer ()
1110
1129
return True
1111
1130
1112
1131
return False
@@ -1119,7 +1138,7 @@ def expect_keyword(self, value: str) -> None:
1119
1138
"""
1120
1139
token = self ._lexer .token
1121
1140
if token .kind == TokenKind .NAME and token .value == value :
1122
- self ._lexer . advance ()
1141
+ self .advance_lexer ()
1123
1142
else :
1124
1143
raise GraphQLSyntaxError (
1125
1144
self ._lexer .source ,
@@ -1135,7 +1154,7 @@ def expect_optional_keyword(self, value: str) -> bool:
1135
1154
"""
1136
1155
token = self ._lexer .token
1137
1156
if token .kind == TokenKind .NAME and token .value == value :
1138
- self ._lexer . advance ()
1157
+ self .advance_lexer ()
1139
1158
return True
1140
1159
1141
1160
return False
@@ -1223,6 +1242,20 @@ def delimited_many(
1223
1242
break
1224
1243
return nodes
1225
1244
1245
+ def advance_lexer (self ) -> None :
1246
+ """Advance the lexer."""
1247
+ token = self ._lexer .advance ()
1248
+ max_tokens = self ._max_tokens
1249
+ if max_tokens is not None and token .kind is not TokenKind .EOF :
1250
+ self ._token_counter += 1
1251
+ if self ._token_counter > max_tokens :
1252
+ raise GraphQLSyntaxError (
1253
+ self ._lexer .source ,
1254
+ token .start ,
1255
+ f"Document contains more that { max_tokens } tokens."
1256
+ " Parsing aborted." ,
1257
+ )
1258
+
1226
1259
1227
1260
def get_token_desc (token : Token ) -> str :
1228
1261
"""Describe a token as a string for debugging."""
0 commit comments