Skip to content

Commit e22d4a0

Browse files
[3.13] gh-156894: Fix the position of syntax errors which cover a range (GH-156901) (GH-156928)
The callers of _PyTokenizer_syntaxerror_known_range() pass columns in bytes, but SyntaxError.offset and end_offset are columns in characters. (cherry picked from commit 59a6913) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8e09add commit e22d4a0

3 files changed

Lines changed: 39 additions & 0 deletions

File tree

Lib/test/test_exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,23 @@ def test_error_offset_continuation_characters(self):
230230
check = self.check
231231
check('"\\\n"(1 for c in I,\\\n\\', 2, 2)
232232

233+
def testSyntaxErrorRange(self):
234+
# gh-156894: the position was reported in bytes, not in characters,
235+
# for the errors which cover a range
236+
for source, offset, end_offset in [
237+
('abcd = 00010', 8, 11),
238+
('\u03b1\u03b2\u03b3\u03b4 = 00010', 8, 11),
239+
('a\u0301b\u0308c\u20d7d\u1ab0 = 00010', 12, 15),
240+
("abcd = ub'a'", 10, 13),
241+
("\u03b1\u03b2\u03b3\u03b4 = ub'a'", 10, 13),
242+
("a\u0301b\u0308c\u20d7d\u1ab0 = ub'a'", 14, 17),
243+
]:
244+
with self.subTest(source=source):
245+
with self.assertRaises(SyntaxError) as cm:
246+
compile(source, '<testcase>', 'exec')
247+
self.assertEqual(cm.exception.offset, offset)
248+
self.assertEqual(cm.exception.end_offset, end_offset)
249+
233250
def testSyntaxErrorOffset(self):
234251
check = self.check
235252
check('def fact(x):\n\treturn x!\n', 2, 10)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix the position of syntax errors which cover a range if the line contains
2+
non-ASCII characters before the error.

Parser/tokenizer/helpers.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@
77

88
/* ############## ERRORS ############## */
99

10+
/* Convert a 1-based column in bytes into a 1-based column in characters.
11+
The line is UTF-8 encoded, so it is enough to skip continuation bytes. */
12+
static int
13+
byte_col_to_char_col(const char *line, int byte_col)
14+
{
15+
int char_col = 1;
16+
for (int i = 0; i < byte_col - 1; i++) {
17+
if ((line[i] & 0xC0) != 0x80) {
18+
char_col++;
19+
}
20+
}
21+
return char_col;
22+
}
23+
1024
static int
1125
_syntaxerror_range(struct tok_state *tok, const char *format,
1226
int col_offset, int end_col_offset,
@@ -33,9 +47,15 @@ _syntaxerror_range(struct tok_state *tok, const char *format,
3347
if (col_offset == -1) {
3448
col_offset = (int)PyUnicode_GET_LENGTH(errtext);
3549
}
50+
else if (col_offset > 0) {
51+
col_offset = byte_col_to_char_col(tok->line_start, col_offset);
52+
}
3653
if (end_col_offset == -1) {
3754
end_col_offset = col_offset;
3855
}
56+
else if (end_col_offset > 0) {
57+
end_col_offset = byte_col_to_char_col(tok->line_start, end_col_offset);
58+
}
3959

4060
Py_ssize_t line_len = strcspn(tok->line_start, "\n");
4161
if (line_len != tok->cur - tok->line_start) {

0 commit comments

Comments
 (0)