Skip to content

Commit 3be14e2

Browse files
committed
Preserve UTF-8 diagnostic columns after tokenizer API cutover
1 parent eb6c7bf commit 3be14e2

3 files changed

Lines changed: 25 additions & 9 deletions

File tree

Lib/test/test_source_encoding.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import unittest
44
from test import support
55
from test.support import script_helper
6-
from test.support.os_helper import TESTFN, unlink, rmtree
7-
from test.support.import_helper import unload
6+
from test.support.os_helper import TESTFN, TESTFN_ASCII, unlink, rmtree
7+
from test.support.import_helper import import_module, unload
88
import importlib
99
import os
1010
import sys
@@ -83,12 +83,30 @@ def test_truncated_utf8_at_eof(self):
8383
self.assertRaises(SyntaxError, compile, seq, '<test>', 'exec')
8484

8585
def test_invalid_utf8_offset_after_non_ascii(self):
86+
for name in ('é', 'éé', '𝒜'):
87+
with self.subTest(name=name):
88+
source = ('x = ' + name).encode() + b'\xff\n'
89+
with self.assertRaises(SyntaxError) as caught:
90+
compile(source, '<test>', 'exec')
91+
error = caught.exception
92+
self.assertEqual(
93+
(error.lineno, error.offset, error.end_lineno, error.end_offset),
94+
(1, 5 + len(name), 1, 5 + len(name)),
95+
)
96+
97+
@support.cpython_only
98+
def test_invalid_utf8_file_offset_after_non_ascii(self):
99+
_testcapi = import_module('_testcapi')
100+
self.addCleanup(unlink, TESTFN_ASCII)
101+
with open(TESTFN_ASCII, 'wb') as f:
102+
f.write(b'\nx = \xc3\xa9\xc3\xa9\xff\n')
86103
with self.assertRaises(SyntaxError) as caught:
87-
compile(b"x = \xc3\xa9\xff\n", "<test>", "exec")
104+
_testcapi.run_file(
105+
os.fsencode(TESTFN_ASCII), _testcapi.Py_file_input, {})
88106
error = caught.exception
89107
self.assertEqual(
90108
(error.lineno, error.offset, error.end_lineno, error.end_offset),
91-
(1, 6, 1, 6),
109+
(2, 7, 2, 7),
92110
)
93111

94112
def test_long_bom_conflict_message_is_not_truncated(self):

Parser/tokenizer/helpers.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,24 +322,22 @@ _PyTokenizer_ensure_utf8(const char *line, struct tok_state *tok, int lineno)
322322
const char *badchar = NULL;
323323
const char *c;
324324
int length;
325-
int col_offset = 0;
326325
const char *line_start = line;
327326
for (c = line; *c; c += length) {
328327
if (!(length = valid_utf8((const unsigned char *)c))) {
329328
badchar = c;
330329
break;
331330
}
332-
col_offset++;
333331
if (*c == '\n') {
334332
lineno++;
335-
col_offset = 0;
336333
line_start = c + 1;
337334
}
338335
}
339336
if (badchar) {
337+
int col_offset = (int)(badchar - line_start) + 1;
340338
_PyTokenizer_syntaxerror_at(
341339
tok, line_start, badchar - line_start, lineno,
342-
col_offset + 1, col_offset + 1,
340+
col_offset, col_offset,
343341
"Non-UTF-8 code starting with '\\x%.2x'"
344342
"%s%V on line %i, "
345343
"but no encoding declared; "

Parser/tokenizer/source.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PyAPI_FUNC(_PyTok_Off) _PyTok_SourceAppendLine(
3737
/* Return borrowed bytes excluding '\n', writing the byte length to *len.
3838
Line numbers are 1-based and clamp to the first or final line; a trailing
3939
'\n' adds an empty final line. The view need not be NUL-terminated.
40-
This does not set an exception. Append and clear invalidate the view. */
40+
This does not set an exception. Append, discard, and clear invalidate the view. */
4141
PyAPI_FUNC(const char *) _PyTok_SourceLineView(
4242
const _PyTok_SourceText *source, Py_ssize_t lineno, Py_ssize_t *len);
4343
/* Return false for invalid line numbers and the virtual EOF line. */

0 commit comments

Comments
 (0)