73
73
def parse (
74
74
source : SourceType ,
75
75
no_location : bool = False ,
76
+ max_tokens : Optional [int ] = None ,
76
77
allow_legacy_fragment_variables : bool = False ,
77
78
) -> DocumentNode :
78
79
"""Given a GraphQL source, parse it into a Document.
79
80
80
81
Throws GraphQLError if a syntax error is encountered.
81
82
82
83
By default, the parser creates AST nodes that know the location in the source that
83
- they correspond to. The ``no_location`` option disables that behavior for
84
- performance or testing.
84
+ they correspond to. Setting the ``no_location`` parameter to False disables that
85
+ behavior for performance or testing.
86
+
87
+ Parser CPU and memory usage is linear to the number of tokens in a document,
88
+ however in extreme cases it becomes quadratic due to memory exhaustion.
89
+ Parsing happens before validation, so even invalid queries can burn lots of
90
+ CPU time and memory. To prevent this, you can set a maximum number of tokens
91
+ allowed within a document using the ``max_tokens`` parameter.
85
92
86
93
Legacy feature (will be removed in v3.3):
87
94
@@ -100,6 +107,7 @@ def parse(
100
107
parser = Parser (
101
108
source ,
102
109
no_location = no_location ,
110
+ max_tokens = max_tokens ,
103
111
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
104
112
)
105
113
return parser .parse_document ()
@@ -108,6 +116,7 @@ def parse(
108
116
def parse_value (
109
117
source : SourceType ,
110
118
no_location : bool = False ,
119
+ max_tokens : Optional [int ] = None ,
111
120
allow_legacy_fragment_variables : bool = False ,
112
121
) -> ValueNode :
113
122
"""Parse the AST for a given string containing a GraphQL value.
@@ -123,6 +132,7 @@ def parse_value(
123
132
parser = Parser (
124
133
source ,
125
134
no_location = no_location ,
135
+ max_tokens = max_tokens ,
126
136
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
127
137
)
128
138
parser .expect_token (TokenKind .SOF )
@@ -134,6 +144,7 @@ def parse_value(
134
144
def parse_const_value (
135
145
source : SourceType ,
136
146
no_location : bool = False ,
147
+ max_tokens : Optional [int ] = None ,
137
148
allow_legacy_fragment_variables : bool = False ,
138
149
) -> ConstValueNode :
139
150
"""Parse the AST for a given string containing a GraphQL constant value.
@@ -144,6 +155,7 @@ def parse_const_value(
144
155
parser = Parser (
145
156
source ,
146
157
no_location = no_location ,
158
+ max_tokens = max_tokens ,
147
159
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
148
160
)
149
161
parser .expect_token (TokenKind .SOF )
@@ -155,6 +167,7 @@ def parse_const_value(
155
167
def parse_type (
156
168
source : SourceType ,
157
169
no_location : bool = False ,
170
+ max_tokens : Optional [int ] = None ,
158
171
allow_legacy_fragment_variables : bool = False ,
159
172
) -> TypeNode :
160
173
"""Parse the AST for a given string containing a GraphQL Type.
@@ -170,6 +183,7 @@ def parse_type(
170
183
parser = Parser (
171
184
source ,
172
185
no_location = no_location ,
186
+ max_tokens = max_tokens ,
173
187
allow_legacy_fragment_variables = allow_legacy_fragment_variables ,
174
188
)
175
189
parser .expect_token (TokenKind .SOF )
@@ -191,13 +205,16 @@ class Parser:
191
205
"""
192
206
193
207
_lexer : Lexer
194
- _no_Location : bool
208
+ _no_location : bool
209
+ _max_tokens : Optional [int ]
195
210
_allow_legacy_fragment_variables : bool
211
+ _token_counter : int
196
212
197
213
def __init__ (
198
214
self ,
199
215
source : SourceType ,
200
216
no_location : bool = False ,
217
+ max_tokens : Optional [int ] = None ,
201
218
allow_legacy_fragment_variables : bool = False ,
202
219
):
203
220
source = (
@@ -206,7 +223,9 @@ def __init__(
206
223
207
224
self ._lexer = Lexer (source )
208
225
self ._no_location = no_location
226
+ self ._max_tokens = max_tokens
209
227
self ._allow_legacy_fragment_variables = allow_legacy_fragment_variables
228
+ self ._token_counter = 0
210
229
211
230
def parse_name (self ) -> NameNode :
212
231
"""Convert a name lex token into a name parse node."""
@@ -477,7 +496,7 @@ def parse_value_literal(self, is_const: bool) -> ValueNode:
477
496
478
497
def parse_string_literal (self , _is_const : bool = False ) -> StringValueNode :
479
498
token = self ._lexer .token
480
- self ._lexer . advance ()
499
+ self .advance_lexer ()
481
500
return StringValueNode (
482
501
value = token .value ,
483
502
block = token .kind == TokenKind .BLOCK_STRING ,
@@ -514,18 +533,18 @@ def parse_object(self, is_const: bool) -> ObjectValueNode:
514
533
515
534
def parse_int (self , _is_const : bool = False ) -> IntValueNode :
516
535
token = self ._lexer .token
517
- self ._lexer . advance ()
536
+ self .advance_lexer ()
518
537
return IntValueNode (value = token .value , loc = self .loc (token ))
519
538
520
539
def parse_float (self , _is_const : bool = False ) -> FloatValueNode :
521
540
token = self ._lexer .token
522
- self ._lexer . advance ()
541
+ self .advance_lexer ()
523
542
return FloatValueNode (value = token .value , loc = self .loc (token ))
524
543
525
544
def parse_named_values (self , _is_const : bool = False ) -> ValueNode :
526
545
token = self ._lexer .token
527
546
value = token .value
528
- self ._lexer . advance ()
547
+ self .advance_lexer ()
529
548
if value == "true" :
530
549
return BooleanValueNode (value = True , loc = self .loc (token ))
531
550
if value == "false" :
@@ -1020,7 +1039,7 @@ def expect_token(self, kind: TokenKind) -> Token:
1020
1039
"""
1021
1040
token = self ._lexer .token
1022
1041
if token .kind == kind :
1023
- self ._lexer . advance ()
1042
+ self .advance_lexer ()
1024
1043
return token
1025
1044
1026
1045
raise GraphQLSyntaxError (
@@ -1037,7 +1056,7 @@ def expect_optional_token(self, kind: TokenKind) -> bool:
1037
1056
"""
1038
1057
token = self ._lexer .token
1039
1058
if token .kind == kind :
1040
- self ._lexer . advance ()
1059
+ self .advance_lexer ()
1041
1060
return True
1042
1061
1043
1062
return False
@@ -1050,7 +1069,7 @@ def expect_keyword(self, value: str) -> None:
1050
1069
"""
1051
1070
token = self ._lexer .token
1052
1071
if token .kind == TokenKind .NAME and token .value == value :
1053
- self ._lexer . advance ()
1072
+ self .advance_lexer ()
1054
1073
else :
1055
1074
raise GraphQLSyntaxError (
1056
1075
self ._lexer .source ,
@@ -1066,7 +1085,7 @@ def expect_optional_keyword(self, value: str) -> bool:
1066
1085
"""
1067
1086
token = self ._lexer .token
1068
1087
if token .kind == TokenKind .NAME and token .value == value :
1069
- self ._lexer . advance ()
1088
+ self .advance_lexer ()
1070
1089
return True
1071
1090
1072
1091
return False
@@ -1154,6 +1173,20 @@ def delimited_many(
1154
1173
break
1155
1174
return nodes
1156
1175
1176
+ def advance_lexer (self ) -> None :
1177
+ max_tokens = self ._max_tokens
1178
+ token = self ._lexer .advance ()
1179
+
1180
+ if max_tokens is not None and token .kind != TokenKind .EOF :
1181
+ self ._token_counter += 1
1182
+ if self ._token_counter > max_tokens :
1183
+ raise GraphQLSyntaxError (
1184
+ self ._lexer .source ,
1185
+ token .start ,
1186
+ f"Document contains more than { max_tokens } tokens."
1187
+ " Parsing aborted." ,
1188
+ )
1189
+
1157
1190
1158
1191
def get_token_desc (token : Token ) -> str :
1159
1192
"""Describe a token as a string for debugging."""
0 commit comments