Skip to content

Commit 59a6913

Browse files
gh-156894: Fix the position of syntax errors which cover a range (GH-156901)
The callers of _PyTokenizer_syntaxerror_known_range() pass columns in bytes, but SyntaxError.offset and end_offset are columns in characters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e50d233 commit 59a6913

4 files changed

Lines changed: 40 additions & 1 deletion

File tree

Lib/test/test_exceptions.py

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

236+
def testSyntaxErrorRange(self):
237+
# gh-156894: the position was reported in bytes, not in characters,
238+
# for the errors which cover a range
239+
for source, offset, end_offset in [
240+
('abcd = 00010', 8, 11),
241+
('\u03b1\u03b2\u03b3\u03b4 = 00010', 8, 11),
242+
('a\u0301b\u0308c\u20d7d\u1ab0 = 00010', 12, 15),
243+
("abcd = ub'a'", 8, 10),
244+
("\u03b1\u03b2\u03b3\u03b4 = ub'a'", 8, 10),
245+
("a\u0301b\u0308c\u20d7d\u1ab0 = ub'a'", 12, 14),
246+
]:
247+
with self.subTest(source=source):
248+
with self.assertRaises(SyntaxError) as cm:
249+
compile(source, '<testcase>', 'exec')
250+
self.assertEqual(cm.exception.offset, offset)
251+
self.assertEqual(cm.exception.end_offset, end_offset)
252+
236253
def testSyntaxErrorOffset(self):
237254
check = self.check
238255
check('def fact(x):\n\treturn x!\n', 2, 10)

Lib/test/test_tokenize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2566,7 +2566,7 @@ def test_tolerant_incompatible_prefix_position_after_non_ascii(self):
25662566
self._get_tokens('bé )tf"2 ', extra_tokens=True)
25672567
self.assertEqual(
25682568
caught.exception.args,
2569-
("'f' and 't' prefixes are incompatible", (1, 6)),
2569+
("'f' and 't' prefixes are incompatible", (1, 5)),
25702570
)
25712571

25722572
def test_tolerant_fstring_closer_at_expression_entry_depth(self):
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
@@ -9,6 +9,20 @@
99

1010
/* ############## ERRORS ############## */
1111

12+
/* Convert a 1-based column in bytes into a 1-based column in characters.
13+
The line is UTF-8 encoded, so it is enough to skip continuation bytes. */
14+
static int
15+
byte_col_to_char_col(const char *line, int byte_col)
16+
{
17+
int char_col = 1;
18+
for (int i = 0; i < byte_col - 1; i++) {
19+
if ((line[i] & 0xC0) != 0x80) {
20+
char_col++;
21+
}
22+
}
23+
return char_col;
24+
}
25+
1226
static int
1327
_syntaxerror_range(struct tok_state *tok, const char *format,
1428
int col_offset, int end_col_offset,
@@ -35,9 +49,15 @@ _syntaxerror_range(struct tok_state *tok, const char *format,
3549
if (col_offset == -1) {
3650
col_offset = (int)PyUnicode_GET_LENGTH(errtext);
3751
}
52+
else if (col_offset > 0) {
53+
col_offset = byte_col_to_char_col(tok->line_start, col_offset);
54+
}
3855
if (end_col_offset == -1) {
3956
end_col_offset = col_offset;
4057
}
58+
else if (end_col_offset > 0) {
59+
end_col_offset = byte_col_to_char_col(tok->line_start, end_col_offset);
60+
}
4161

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

0 commit comments

Comments
 (0)