Skip to content

Commit 1fc8e7e

Browse files
committed
assert_valid_name: share character classes with lexer
Replicates graphql/graphql-js@22b9504
1 parent fa23fe3 commit 1fc8e7e

File tree

5 files changed

+74
-28
lines changed

5 files changed

+74
-28
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
__all__ = ["is_digit", "is_letter", "is_name_start", "is_name_continue"]
2+
3+
4+
def is_digit(char: str) -> bool:
5+
"""Check whether char is a digit
6+
7+
For internal use by the lexer only.
8+
"""
9+
return "0" <= char <= "9"
10+
11+
12+
def is_letter(char: str) -> bool:
13+
"""Check whether char is a plain ASCII letter
14+
15+
For internal use by the lexer only.
16+
"""
17+
return "A" <= char <= "Z" or "a" <= char <= "z"
18+
19+
20+
def is_name_start(char: str) -> bool:
21+
"""Check whether char is allowed at the beginning of a GraphQL name
22+
23+
For internal use by the lexer only.
24+
"""
25+
return is_letter(char) or char == "_"
26+
27+
28+
def is_name_continue(char: str) -> bool:
29+
"""Check whether char is allowed in the continuation of a GraphQL name
30+
31+
For internal use by the lexer only.
32+
"""
33+
return is_letter(char) or is_digit(char) or char == "_"

src/graphql/language/lexer.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from ..error import GraphQLSyntaxError
44
from .ast import Token
55
from .block_string import dedent_block_string_value
6+
from .character_classes import is_digit, is_name_start, is_name_continue
67
from .source import Source
78
from .token_kind import TokenKind
89

@@ -129,10 +130,10 @@ def read_next_token(self, start: int) -> Token:
129130
if kind:
130131
return self.create_token(kind, position, position + 1)
131132

132-
if "0" <= char <= "9" or char == "-":
133+
if is_digit(char) or char == "-":
133134
return self.read_number(position, char)
134135

135-
if "A" <= char <= "Z" or "a" <= char <= "z" or char == "_":
136+
if is_name_start(char):
136137
return self.read_name(position)
137138

138139
if char == ".":
@@ -196,7 +197,7 @@ def read_number(self, start: int, first_char: str) -> Token:
196197
if char == "0":
197198
position += 1
198199
char = body[position : position + 1]
199-
if "0" <= char <= "9":
200+
if is_digit(char):
200201
raise GraphQLSyntaxError(
201202
self.source,
202203
position,
@@ -240,7 +241,7 @@ def read_number(self, start: int, first_char: str) -> Token:
240241

241242
def read_digits(self, start: int, first_char: str) -> int:
242243
"""Return the new position in the source after reading one or more digits."""
243-
if not "0" <= first_char <= "9":
244+
if not is_digit(first_char):
244245
raise GraphQLSyntaxError(
245246
self.source,
246247
start,
@@ -251,7 +252,7 @@ def read_digits(self, start: int, first_char: str) -> int:
251252
body = self.source.body
252253
body_length = len(body)
253254
position = start + 1
254-
while position < body_length and "0" <= body[position] <= "9":
255+
while position < body_length and is_digit(body[position]):
255256
position += 1
256257
return position
257258

@@ -427,12 +428,7 @@ def read_name(self, start: int) -> Token:
427428

428429
while position < body_length:
429430
char = body[position]
430-
if not (
431-
"A" <= char <= "Z"
432-
or "a" <= char <= "z"
433-
or "0" <= char <= "9"
434-
or char == "_"
435-
):
431+
if not is_name_continue(char):
436432
break
437433
position += 1
438434

@@ -558,8 +554,3 @@ def is_supplementary_code_point(body: str, location: int) -> bool:
558554

559555
def decode_surrogate_pair(leading: int, trailing: int) -> int:
560556
return 0x10000 + (((leading & 0x03FF) << 10) | (trailing & 0x03FF))
561-
562-
563-
def is_name_start(char: str) -> bool:
564-
"""Check whether char is an underscore or a plain ASCII letter"""
565-
return char == "_" or "A" <= char <= "Z" or "a" <= char <= "z"

src/graphql/utilities/assert_valid_name.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
from typing import Optional
33

4+
from ..language.character_classes import is_name_start, is_name_continue
45
from ..error import GraphQLError
56

67
__all__ = ["assert_valid_name", "is_valid_name_error"]
@@ -21,13 +22,22 @@ def is_valid_name_error(name: str) -> Optional[GraphQLError]:
2122
"""Return an Error if a name is invalid."""
2223
if not isinstance(name, str):
2324
raise TypeError("Expected name to be a string.")
25+
2426
if name.startswith("__"):
2527
return GraphQLError(
2628
f"Name {name!r} must not begin with '__',"
2729
" which is reserved by GraphQL introspection."
2830
)
29-
if not re_name.match(name):
31+
32+
if not name:
33+
return GraphQLError("Expected name to be a non-empty string.")
34+
35+
if not all(is_name_continue(char) for char in name[1:]):
3036
return GraphQLError(
31-
f"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but {name!r} does not."
37+
f"Names must only contain [_a-zA-Z0-9] but {name!r} does not."
3238
)
39+
40+
if not is_name_start(name[0]):
41+
return GraphQLError(f"Names must start with [_a-zA-Z] but {name!r} does not.")
42+
3343
return None

tests/type/test_validation.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ def rejects_an_object_type_with_incorrectly_named_fields():
500500
)
501501
msg = validate_schema(schema)[0].message
502502
assert msg == (
503-
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/"
503+
"Names must only contain [_a-zA-Z0-9]"
504504
" but 'bad-name-with-dashes' does not."
505505
)
506506

@@ -534,7 +534,7 @@ def rejects_field_args_with_invalid_names():
534534

535535
msg = validate_schema(schema)[0].message
536536
assert msg == (
537-
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/"
537+
"Names must only contain [_a-zA-Z0-9]"
538538
" but 'bad-name-with-dashes' does not."
539539
)
540540

@@ -1001,20 +1001,16 @@ def schema_with_enum(name: str) -> GraphQLSchema:
10011001

10021002
schema1 = schema_with_enum("#value")
10031003
msg = validate_schema(schema1)[0].message
1004-
assert msg == (
1005-
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but '#value' does not."
1006-
)
1004+
assert msg == ("Names must start with [_a-zA-Z] but '#value' does not.")
10071005

10081006
schema2 = schema_with_enum("1value")
10091007
msg = validate_schema(schema2)[0].message
1010-
assert msg == (
1011-
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but '1value' does not."
1012-
)
1008+
assert msg == ("Names must start with [_a-zA-Z] but '1value' does not.")
10131009

10141010
schema3 = schema_with_enum("KEBAB-CASE")
10151011
msg = validate_schema(schema3)[0].message
10161012
assert msg == (
1017-
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but 'KEBAB-CASE' does not."
1013+
"Names must only contain [_a-zA-Z0-9] but 'KEBAB-CASE' does not."
10181014
)
10191015

10201016
schema4 = schema_with_enum("true")

tests/utilities/test_assert_valid_name.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ def throws_for_non_strings():
2424
msg = str(exc_info.value)
2525
assert msg == "Expected name to be a string."
2626

27+
def throws_on_empty_strings():
28+
with raises(GraphQLError) as exc_info:
29+
assert_valid_name("")
30+
msg = str(exc_info.value)
31+
assert msg == "Expected name to be a non-empty string."
32+
2733
def throws_for_names_with_invalid_characters():
28-
with raises(GraphQLError, match="Names must match"):
34+
with raises(GraphQLError) as exc_info:
2935
assert_valid_name(">--()-->")
36+
msg = str(exc_info.value)
37+
assert msg == "Names must only contain [_a-zA-Z0-9] but '>--()-->' does not."
38+
39+
def throws_for_names_starting_with_invalid_characters():
40+
with raises(GraphQLError) as exc_info:
41+
assert_valid_name("42MeaningsOfLife")
42+
msg = str(exc_info.value)
43+
assert msg == (
44+
"Names must start with [_a-zA-Z] but '42MeaningsOfLife' does not."
45+
)

0 commit comments

Comments
 (0)