Skip to content

Commit 113ad29

Browse files
[3.14] gh-156894: Fix the position of syntax errors which cover a range (GH-156901) (GH-156927)
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 1e54caa commit 113ad29

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
@@ -232,6 +232,23 @@ def test_error_offset_continuation_characters(self):
232232
check = self.check
233233
check('"\\\n"(1 for c in I,\\\n\\', 2, 2)
234234

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

99
/* ############## ERRORS ############## */
1010

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

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

0 commit comments

Comments
 (0)