Skip to content

Commit fbb46ae

Browse files
authored
Error on unicode surrogates in mypyc (#21936)
This PR is expected to fail until mypyc/ast_serialize#82 is merged and released.
1 parent 24457a0 commit fbb46ae

8 files changed

Lines changed: 75 additions & 18 deletions

File tree

mypy/nativeparse.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def parse_to_binary_ast(
295295
platform=options.platform,
296296
always_true=options.always_true,
297297
always_false=options.always_false,
298-
cache_version=4,
298+
cache_version=5,
299299
)
300300
return (
301301
ast_bytes,
@@ -1348,6 +1348,7 @@ def read_expression(state: State, data: ReadBuffer) -> Expression:
13481348
return ce
13491349
elif tag == nodes.STR_EXPR:
13501350
se = StrExpr(read_str(data))
1351+
se.has_surrogates = read_bool(data)
13511352
read_loc(data, se)
13521353
expect_end_tag(data)
13531354
return se
@@ -1437,7 +1438,7 @@ def read_expression(state: State, data: ReadBuffer) -> Expression:
14371438
s = StrExpr(read_str(data))
14381439
read_loc(data, s)
14391440
fitems.append(s)
1440-
expr = build_fstring_join(data, fitems)
1441+
expr = build_fstring_join(data, fitems, set_has_surrogates=True)
14411442
expect_end_tag(data)
14421443
return expr
14431444
elif tag == nodes.LIST_COMPREHENSION:
@@ -1534,6 +1535,7 @@ def read_expression(state: State, data: ReadBuffer) -> Expression:
15341535
read_loc(data, s)
15351536
titems.append(s)
15361537
expr = TemplateStrExpr(titems)
1538+
expr.has_surrogates = read_bool(data)
15371539
read_loc(data, expr)
15381540
state.check_min_version(
15391541
"t-strings", (3, 14), expr.line, expr.column, enforce_in_stubs=True
@@ -1660,16 +1662,30 @@ def read_fstring_items(state: State, data: ReadBuffer) -> Expression:
16601662
return build_fstring_join(data, items)
16611663

16621664

1663-
def build_fstring_join(data: ReadBuffer, items: list[Expression]) -> Expression:
1665+
def build_fstring_join(
1666+
data: ReadBuffer, items: list[Expression], set_has_surrogates: bool = False
1667+
) -> Expression:
16641668
items = collapse_consecutive_str_items(items)
16651669
if len(items) == 1:
16661670
expr = items[0]
1671+
if set_has_surrogates:
1672+
if isinstance(expr, StrExpr):
1673+
target = expr
1674+
else:
1675+
assert isinstance(expr, CallExpr) and isinstance(expr.callee, MemberExpr)
1676+
# It doesn't really matter where to set the surrogates flag,
1677+
# so we set it on the outermost format string.
1678+
target = expr.callee.expr
1679+
assert isinstance(target, StrExpr)
1680+
target.has_surrogates = read_bool(data)
16671681
read_loc(data, expr)
16681682
return expr
16691683
args = ListExpr(items)
16701684
str_expr = StrExpr("")
16711685
member = MemberExpr(str_expr, "join")
16721686
call = CallExpr(member, [args], [ARG_POS], [None])
1687+
if set_has_surrogates:
1688+
str_expr.has_surrogates = read_bool(data)
16731689
read_loc(data, call)
16741690
set_line_column(args, call)
16751691
set_line_column(str_expr, call)

mypy/nodes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,7 +2275,7 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
22752275
class StrExpr(Expression):
22762276
"""String literal"""
22772277

2278-
__slots__ = ("value", "as_type")
2278+
__slots__ = ("value", "as_type", "has_surrogates")
22792279

22802280
__match_args__ = ("value",)
22812281

@@ -2284,11 +2284,16 @@ class StrExpr(Expression):
22842284
# represents the type denoted by the type expression.
22852285
# None means "is not a type expression".
22862286
as_type: NotParsed | mypy.types.Type | None
2287+
# This indicates whether original string literal contained Unicode surrogate
2288+
# codepoints. Those are not supported by Ruff parser and are replaced by
2289+
# replacement characters. Thus, we can't support them in mypyc.
2290+
has_surrogates: bool
22872291

22882292
def __init__(self, value: str) -> None:
22892293
super().__init__()
22902294
self.value = value
22912295
self.as_type = NotParsed.VALUE
2296+
self.has_surrogates = False
22922297

22932298
def accept(self, visitor: ExpressionVisitor[T]) -> T:
22942299
return visitor.visit_str_expr(self)
@@ -2937,7 +2942,7 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
29372942
class TemplateStrExpr(Expression):
29382943
"""Template string expression t'...'."""
29392944

2940-
__slots__ = ("items",)
2945+
__slots__ = ("items", "has_surrogates")
29412946
__match_args__ = ("items",)
29422947

29432948
# Each item is either:
@@ -2946,12 +2951,14 @@ class TemplateStrExpr(Expression):
29462951
# where conversion is str | None ("r", "s", "a", or None)
29472952
# and format_spec_expr is Expression | None
29482953
items: list[Expression | tuple[Expression, str, str | None, Expression | None]]
2954+
has_surrogates: bool
29492955

29502956
def __init__(
29512957
self, items: list[Expression | tuple[Expression, str, str | None, Expression | None]]
29522958
) -> None:
29532959
super().__init__()
29542960
self.items = items
2961+
self.has_surrogates = False
29552962

29562963
def accept(self, visitor: ExpressionVisitor[T]) -> T:
29572964
return visitor.visit_template_str_expr(self)

mypy/test/test_nativeparse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ def _assert_trivial_binary_data(self, b: bytes, /) -> None:
247247
def int_enc(n: int) -> int:
248248
return (n + 10) << 1
249249

250+
def bool_enc(b: bool) -> int:
251+
return int(b)
252+
250253
def locs(start_line: int, start_column: int, end_line: int, end_column: int) -> list[int]:
251254
return [
252255
LOCATION,
@@ -267,6 +270,7 @@ def locs(start_line: int, start_column: int, end_line: int, end_column: int) ->
267270
+ [END_TAG, LIST_GEN, 22, nodes.STR_EXPR]
268271
+ [LITERAL_STR, int_enc(5)]
269272
+ list(b"hello")
273+
+ [bool_enc(False)] # no unicode surrogates
270274
+ locs(1, 6, 1, 13)
271275
+ [END_TAG]
272276
# arg_kinds: [ARG_POS]

mypyc/irbuild/prebuildvisitor.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
MypyFile,
1616
NameExpr,
1717
Node,
18+
StrExpr,
1819
SymbolNode,
1920
Var,
2021
)
@@ -259,6 +260,17 @@ def visit_name_expr(self, expr: NameExpr) -> None:
259260
if isinstance(expr.node, (Var, FuncDef)):
260261
self.visit_symbol_node(expr.node)
261262

263+
def visit_str_expr(self, o: StrExpr) -> None:
264+
# Handle surrogates before main pass to avoid conflicts with various optimizations
265+
# like replacing `ord("<some char>")` with its integer value statically, etc.
266+
if o.has_surrogates:
267+
self.errors.error(
268+
"Surrogate codepoints in string literals not supported, use chr(...) instead",
269+
self.current_file.path,
270+
o.line,
271+
)
272+
super().visit_str_expr(o)
273+
262274
def visit_var(self, var: Var) -> None:
263275
self.visit_symbol_node(var)
264276

@@ -272,7 +284,7 @@ def visit_symbol_node(self, symbol: SymbolNode) -> None:
272284
orig_func = self.symbols_to_funcs[symbol]
273285
if self.is_parent(self.funcs[-1], orig_func):
274286
# The function in which the symbol was previously seen is
275-
# nested within the function currently being visited. Thus
287+
# nested within the function currently being visited. Thus,
276288
# the current function is a better candidate to contain the
277289
# declaration.
278290
self.symbols_to_funcs[symbol] = self.funcs[-1]

mypyc/irbuild/visitor.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ def visit_dict_expr(self, expr: DictExpr) -> Value:
313313
return transform_dict_expr(self.builder, expr)
314314

315315
def visit_template_str_expr(self, expr: TemplateStrExpr) -> Value:
316+
if expr.has_surrogates:
317+
self.builder.error(
318+
"Surrogate codepoints in string literals not supported, use chr(...) instead",
319+
expr.line,
320+
)
316321
self.bail("Template strings are not supported by mypyc", expr.line)
317322

318323
def visit_set_expr(self, expr: SetExpr) -> Value:

mypyc/test-data/irbuild-str.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,3 +1175,18 @@ L0:
11751175
r1 = ''
11761176
r2 = CPyStr_EqualLiteral(r0, r1, 0)
11771177
return r2
1178+
1179+
[case testUnicodeSurrogate]
1180+
# flags: --native-parser
1181+
1182+
def f() -> str:
1183+
return "\ud800"
1184+
1185+
def test_surrogate() -> None:
1186+
assert ord(f()) == 0xd800
1187+
assert ord("\udfff") == 0xdfff
1188+
assert repr("foobar\x00\xab\ud912\U00012345") == r"'foobar\x00«\ud912𒍅'"
1189+
[out]
1190+
main:4: error: Surrogate codepoints in string literals not supported, use chr(...) instead
1191+
main:8: error: Surrogate codepoints in string literals not supported, use chr(...) instead
1192+
main:9: error: Surrogate codepoints in string literals not supported, use chr(...) instead

mypyc/test-data/run-strings.test

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,15 +1099,6 @@ def test_encode() -> None:
10991099
with assertRaises(UnicodeEncodeError):
11001100
u.encode('latin1')
11011101

1102-
[case testUnicodeSurrogate]
1103-
def f() -> str:
1104-
return "\ud800"
1105-
1106-
def test_surrogate() -> None:
1107-
assert ord(f()) == 0xd800
1108-
assert ord("\udfff") == 0xdfff
1109-
assert repr("foobar\x00\xab\ud912\U00012345") == r"'foobar\x00«\ud912𒍅'"
1110-
11111102
[case testStrip]
11121103
def test_all_strips_default() -> None:
11131104
s = " a1\t"

mypyc/test/testutil.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from mypy import build
1313
from mypy.errors import CompileError
14+
from mypy.main import process_options
1415
from mypy.nodes import Expression, MypyFile
1516
from mypy.options import Options
1617
from mypy.test.config import test_temp_dir
@@ -102,10 +103,16 @@ def build_ir_for_single_file2(
102103
) -> tuple[ModuleIR, MypyFile, dict[Expression, Type], Mapper]:
103104
program_text = "\n".join(input_lines)
104105

105-
# By default generate IR compatible with the earliest supported Python C API.
106+
flags = re.search("# flags: (.*)$", program_text, flags=re.MULTILINE)
107+
108+
# By default, generate IR compatible with the earliest supported Python C API.
106109
# If a test needs more recent API features, this should be overridden.
107110
compiler_options = compiler_options or CompilerOptions(capi_version=(3, 10))
108-
options = Options()
111+
if flags:
112+
flag_list = flags.group(1).split()
113+
_, options = process_options(flag_list, require_targets=False)
114+
else:
115+
options = Options()
109116
options.show_traceback = True
110117
options.hide_error_codes = True
111118
options.use_builtins_fixtures = True
@@ -120,7 +127,7 @@ def build_ir_for_single_file2(
120127
options.per_module_options["__main__"] = {"mypyc": True}
121128

122129
source = build.BuildSource("main", "__main__", program_text)
123-
# Construct input as a single single.
130+
# Construct input as a single source.
124131
# Parse and type check the input program.
125132
result = build.build(sources=[source], options=options, alt_lib_path=test_temp_dir)
126133
result.manager.metastore.close()

0 commit comments

Comments
 (0)