Skip to content

Commit 3f78561

Browse files
committed
gh-153569: Use logical spans in tokenizer views and column calculations
1 parent 3e32141 commit 3f78561

4 files changed

Lines changed: 52 additions & 37 deletions

File tree

‎Lib/test/test_tokenize.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,6 +2498,34 @@ def test_fstring_offsets_survive_buffer_reallocation(self):
24982498
chunks.__next__, extra_tokens=extra_tokens))
24992499
self.assertEqual(tokens, expected)
25002500

2501+
def test_multiline_unicode_columns_after_source_discard(self):
2502+
# The first line is discarded before the multiline token is read.
2503+
# Its saved offsets must remain valid when the source buffer grows.
2504+
padding = "é" * 9000
2505+
source = f"pass\né = '''{padding}\n漢''' + ö\nß = 1\n"
2506+
expected = [
2507+
(token.NAME, "pass", (1, 0), (1, 4)),
2508+
(token.NAME, "é", (2, 0), (2, 1)),
2509+
(token.STRING, f"'''{padding}\n漢'''", (2, 4), (3, 4)),
2510+
(token.NAME, "ö", (3, 7), (3, 8)),
2511+
(token.NAME, "ß", (4, 0), (4, 1)),
2512+
]
2513+
for extra_tokens in (False, True):
2514+
for encoding in (None, "utf-8"):
2515+
with self.subTest(extra_tokens=extra_tokens, encoding=encoding):
2516+
stream = (StringIO(source) if encoding is None
2517+
else BytesIO(source.encode(encoding)))
2518+
tokens = list(tokenize._generate_tokens_from_c_tokenizer(
2519+
stream.readline, extra_tokens=extra_tokens,
2520+
encoding=encoding))
2521+
self.assertEqual(
2522+
[t[:4] for t in tokens
2523+
if t.type in (token.NAME, token.STRING)],
2524+
expected,
2525+
)
2526+
string = next(t for t in tokens if t.type == token.STRING)
2527+
self.assertEqual(string.line, f"é = '''{padding}\n漢''' + ö\n")
2528+
25012529
def test_extra_tokens_relaxes_lexer_errors(self):
25022530
cases = [
25032531
(

‎Parser/tokenizer/api.c‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,14 @@ _PyToken_GetView(const struct tok_state *tok, const struct token *token,
6060
_PyToken_View *view)
6161
{
6262
assert(view != NULL);
63-
assert((token->span.start == -1 && token->span.end == -1) ||
64-
_PyTok_SpanIsValid(token->span));
65-
if (token->span.start >= 0) {
66-
(void)_PyTok_SourcePointer(&tok->source, token->span.end);
67-
}
68-
view->text = token->span.start < 0
69-
? NULL : _PyTok_SourcePointer(&tok->source, token->span.start);
70-
view->length = token->span.end - token->span.start;
71-
view->end_line = _PyTok_SourcePointer(&tok->source, tok->line_start);
72-
view->line = ISSTRINGLIT(token->type)
73-
? view->text - token->start_loc.byte_col : view->end_line;
74-
view->line_length = tok->inp - tok->line_start +
75-
(view->end_line - view->line);
63+
view->text = _PyToken_TextView(tok, token, &view->length);
64+
view->end_line_start = tok->line_start;
65+
view->line_span = (_PyTok_Span){
66+
ISSTRINGLIT(token->type)
67+
? token->span.start - token->start_loc.byte_col : tok->line_start,
68+
tok->inp,
69+
};
70+
view->line = _PyTok_SourcePointer(&tok->source, view->line_span.start);
7671
view->implicit_newline = tok->implicit_newline;
7772
view->at_eof = tok->done == E_EOF;
7873
}

‎Parser/tokenizer/tokenizer.h‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ typedef struct {
2222
const char *text;
2323
Py_ssize_t length;
2424
const char *line;
25-
Py_ssize_t line_length;
26-
const char *end_line;
25+
_PyTok_Span line_span;
26+
_PyTok_Off end_line_start;
2727
int implicit_newline;
2828
int at_eof;
2929
} _PyToken_View;
@@ -73,8 +73,9 @@ _PyTokenizer_Info _PyTokenizer_GetInfo(const struct tok_state *);
7373
/* An absent token span has a nonnull empty text view. */
7474
const char *_PyToken_TextView(
7575
const struct tok_state *, const struct token *, Py_ssize_t *);
76-
/* Use the token from the most recent Get. text is NULL for an absent span;
77-
line includes the token's complete physical line range. */
76+
/* Use the token from the most recent Get. An absent span has nonnull empty text.
77+
line contains the bytes of line_span, the token's complete physical line
78+
range. end_line_start is the logical offset of its final physical line. */
7879
void _PyToken_GetView(
7980
const struct tok_state *tok, const struct token *token,
8081
_PyToken_View *view);

‎Python/Python-tokenize.c‎

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,13 @@ _get_col_offsets(tokenizeriterobject *it, const struct token *token,
180180
Py_ssize_t *col_offset, Py_ssize_t *end_col_offset)
181181
{
182182
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(it);
183-
const char *token_start = view->text;
184-
const char *token_end = token_start == NULL
185-
? NULL : token_start + view->length;
183+
_PyTok_Off token_start = token->span.start;
184+
_PyTok_Off token_end = token->span.end;
186185
Py_ssize_t lineno = token->start_loc.lineno;
187186
Py_ssize_t end_lineno = token->end_loc.lineno;
188187
Py_ssize_t byte_offset = -1;
189-
if (token_start != NULL && token_start >= view->line) {
190-
byte_offset = token_start - view->line;
188+
if (token_start >= view->line_span.start) {
189+
byte_offset = token_start - view->line_span.start;
191190
if (line_changed) {
192191
*col_offset = _PyPegen_byte_offset_to_character_offset_line(line, 0, byte_offset);
193192
if (*col_offset < 0) {
@@ -200,8 +199,8 @@ _get_col_offsets(tokenizeriterobject *it, const struct token *token,
200199
}
201200
}
202201

203-
if (token_end != NULL && token_end >= view->end_line) {
204-
Py_ssize_t end_byte_offset = token_end - view->end_line;
202+
if (token_end >= view->end_line_start) {
203+
Py_ssize_t end_byte_offset = token_end - view->end_line_start;
205204
if (lineno == end_lineno) {
206205
// Avoid rescanning the prefix of a very long line.
207206
Py_ssize_t token_col_offset = _PyPegen_byte_offset_to_character_offset_line(line, byte_offset, end_byte_offset);
@@ -213,7 +212,8 @@ _get_col_offsets(tokenizeriterobject *it, const struct token *token,
213212
}
214213
else {
215214
*end_col_offset = _PyPegen_byte_offset_to_character_offset_line(
216-
line, view->end_line - view->line, token_end - view->line);
215+
line, view->end_line_start - view->line_span.start,
216+
token_end - view->line_span.start);
217217
if (*end_col_offset < 0) {
218218
return -1;
219219
}
@@ -251,15 +251,7 @@ tokenizeriter_next(PyObject *op)
251251
}
252252
_PyToken_View view;
253253
_PyToken_GetView(it->tok, &token, &view);
254-
const char *token_start = view.text;
255-
PyObject *str;
256-
if (token.span.start < 0) {
257-
assert(token.span.start == -1 && token.span.end == -1);
258-
str = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
259-
}
260-
else {
261-
str = PyUnicode_FromStringAndSize(token_start, view.length);
262-
}
254+
PyObject *str = PyUnicode_FromStringAndSize(view.text, view.length);
263255
if (str == NULL) {
264256
goto exit;
265257
}
@@ -271,7 +263,7 @@ tokenizeriter_next(PyObject *op)
271263
if (it->extra_tokens && is_trailing_token) {
272264
line = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
273265
} else {
274-
Py_ssize_t size = view.line_length;
266+
Py_ssize_t size = view.line_span.end - view.line_span.start;
275267
if (size >= 1 && view.implicit_newline) {
276268
size -= 1;
277269
}
@@ -307,8 +299,7 @@ tokenizeriter_next(PyObject *op)
307299
else if (type == NEWLINE) {
308300
if (!view.implicit_newline) {
309301
Py_DECREF(str);
310-
assert(token_start != NULL);
311-
if (token_start[0] == '\r') {
302+
if (view.text[0] == '\r') {
312303
str = PyUnicode_FromString("\r\n");
313304
} else {
314305
str = PyUnicode_FromString("\n");

0 commit comments

Comments
 (0)