Skip to content

Commit 301790f

Browse files
committed
gh-159465: Track newline effects and preserve parser layout
1 parent 589feb4 commit 301790f

7 files changed

Lines changed: 64 additions & 8 deletions

File tree

‎Lib/test/test_codeop.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,34 @@ def test_incomplete_string_diagnostics(self, symbol, prefix):
124124
self.assertEqual(cm.exception.args, (
125125
'incomplete input', ('<input>', 2, 9, text, 2, -1)))
126126

127+
@subTests('compiler', COMPILERS)
128+
@subTests('newline', ('\n', '\r', '\r\n'))
129+
def test_unterminated_trailing_line(self, compiler, newline):
130+
for head, symbol in (('1', 'eval'), ('if 1: pass', 'single'),
131+
('if 1:\n pass', 'single')):
132+
for tail in (' ', '\t', '\f ', '\n '):
133+
source = head + newline + tail
134+
with self.subTest(source=source, symbol=symbol):
135+
if compiler in RAW_COMPILERS:
136+
with self.assertRaises(_IncompleteInputError):
137+
compiler(source, '<input>', symbol)
138+
else:
139+
self.assertIsNone(compiler(source, '<input>', symbol))
140+
self.assertValid(source + '\n', symbol, compiler=compiler)
141+
for tail in ('\f', ' \f', '\n\f'):
142+
with self.subTest(head=head, tail=tail, symbol=symbol):
143+
self.assertValid(head + newline + tail, symbol,
144+
compiler=compiler)
145+
146+
for tail in ('# comment', ' # comment'):
147+
self.assertValid('if 1: pass' + newline + tail, compiler=compiler)
148+
self.assertValid('1' + newline + tail, 'eval', compiler=compiler)
149+
150+
for head in ('1', 'if 1: pass', 'if 1:\n pass'):
151+
for tail in (' ', '\t', '\f', '# comment'):
152+
self.assertValid(head + newline + tail, 'exec',
153+
compiler=compiler)
154+
127155
@subTests('symbol', ('single', 'exec', 'eval'))
128156
def test_unfinished_token_in_invalid_position(self, symbol):
129157
for source in ('a b', '(a b', '1 """', '(1 """',

‎Parser/lexer/layout.c‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ _PyLexer_ContinueLine(struct tok_state *tok)
2222
}
2323
c = tok_nextc(tok);
2424
if (tok->implicit_newline == _PyTokenizer_APPEND_NEWLINE) {
25-
tok->needs_newline = 1;
25+
tok->needs_newline |= _PyTokenizer_NEWLINE_CONTINUATION;
2626
}
2727
if (c == EOF) {
2828
tok->done = E_EOF;
@@ -117,6 +117,11 @@ _PyLexer_BeginLine(struct tok_state *tok)
117117
}
118118
}
119119
tok_backup(tok, c);
120+
if (c == '\n' && tok->implicit_newline == _PyTokenizer_IMPLY_NEWLINE &&
121+
tok->level == 0 && (cont_line_col || col)) {
122+
// Without the newline, a whitespace-only line affects indentation.
123+
tok->needs_newline |= _PyTokenizer_NEWLINE_INDENT;
124+
}
120125
if (c == '#' || c == '\n' || c == '\r') {
121126
int interactive = _PyTok_ReaderIsInteractive(tok);
122127
/* Lines with only whitespace and/or comments
@@ -174,6 +179,10 @@ _PyLexer_IndentationToken(struct tok_state *tok, struct token *token)
174179
int
175180
_PyLexer_Newline(struct tok_state *tok, struct token *token, int blankline)
176181
{
182+
if (!blankline && tok->implicit_newline == _PyTokenizer_IMPLY_NEWLINE) {
183+
// The statement's terminating newline was not in the input.
184+
tok->needs_newline |= _PyTokenizer_NEWLINE_STATEMENT;
185+
}
177186
tok->layout.at_bol = 1;
178187
if (blankline || tok->level > 0) {
179188
if (!tok->tok_extra_tokens) {

‎Parser/lexer/lexer.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ _PyLexer_get_normal(struct tok_state *tok, ftstring_state *current, struct token
235235
return type;
236236
}
237237
}
238+
if (tok->implicit_newline == _PyTokenizer_IMPLY_NEWLINE &&
239+
tok->layout.depth > 0) {
240+
// Ending this comment lets the next line dedent the suite.
241+
tok->needs_newline |= _PyTokenizer_NEWLINE_STATEMENT;
242+
}
238243
if (tok->tok_extra_tokens) {
239244
tok_backup(tok, c); /* don't eat the newline or EOF */
240245
p_start = comment;

‎Parser/pegen.c‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,10 +1247,12 @@ _PyPegen_run_parser_from_string(const char *str, int start_rule, PyObject *filen
12471247

12481248
result = _PyPegen_run_parser(p);
12491249
if (result != NULL && (parser_flags & PyPARSE_ALLOW_INCOMPLETE_INPUT)) {
1250-
_PyTokenizer_Info info = _PyTokenizer_GetInfo(tok);
1251-
if (info.needs_newline ||
1250+
int needs_newline = _PyTokenizer_GetNewlineRequirements(tok);
1251+
if ((needs_newline & _PyTokenizer_NEWLINE_CONTINUATION) ||
1252+
(start_rule != Py_file_input &&
1253+
(needs_newline & _PyTokenizer_NEWLINE_INDENT)) ||
12521254
(start_rule == Py_single_input && p->input_exhausted &&
1253-
info.implicit_newline == _PyTokenizer_IMPLY_NEWLINE)) {
1255+
(needs_newline & _PyTokenizer_NEWLINE_STATEMENT))) {
12541256
_PyPegen_raise_incomplete_input(p);
12551257
if (incomplete != NULL && PyErr_ExceptionMatches(PyExc_IncompleteInputError)) {
12561258
*incomplete = PyErr_GetRaisedException();

‎Parser/pegen.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ typedef struct {
8888
int n_keyword_lists;
8989
int start_rule;
9090
int *errcode;
91-
int parsing_started;
92-
int input_exhausted;
91+
unsigned char parsing_started;
92+
unsigned char input_exhausted;
9393
PyObject* normalize;
9494
int starting_lineno;
9595
int starting_col_offset;

‎Parser/tokenizer/api.c‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ _PyTokenizer_GetStatus(const struct tok_state *tok)
1313
return tok->done;
1414
}
1515

16+
int
17+
_PyTokenizer_GetNewlineRequirements(const struct tok_state *tok)
18+
{
19+
return tok->needs_newline;
20+
}
21+
1622
_PyTokenizer_Info
1723
_PyTokenizer_GetInfo(const struct tok_state *tok)
1824
{
1925
_PyTokenizer_Info info = {
2026
.status = tok->done,
2127
.implicit_newline = tok->implicit_newline,
22-
.needs_newline = tok->needs_newline,
2328
.diagnostic = tok->diagnostic,
2429
.location = {tok->lineno, tok->line_start < 0
2530
? -1 : (int)(tok->cur - tok->line_start)},

‎Parser/tokenizer/tokenizer.h‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ typedef enum {
1212
_PyTokenizer_APPEND_NEWLINE, // Also append a blank line to terminated input.
1313
} _PyTokenizer_Newline;
1414

15+
/* Reasons that the appended newline can change the token stream. */
16+
enum {
17+
_PyTokenizer_NEWLINE_STATEMENT = 1,
18+
_PyTokenizer_NEWLINE_INDENT = 2,
19+
_PyTokenizer_NEWLINE_CONTINUATION = 4,
20+
};
21+
1522
/* Initialize before use. metadata owns a reference released by _PyToken_Free;
1623
a consumer taking that reference must set metadata to NULL. */
1724
struct token {
@@ -44,7 +51,6 @@ typedef struct {
4451
typedef struct {
4552
int status;
4653
unsigned char implicit_newline; // _PyTokenizer_Newline
47-
unsigned char needs_newline;
4854
_PyTokenizer_Diagnostic diagnostic;
4955
_PyTok_Loc location;
5056
_PyTok_Off cursor;
@@ -80,6 +86,7 @@ _PyToken_Free(struct token *token)
8086
mutated or freed. Source spans may be discarded when reading more input. */
8187
_PyTokenizer_Info _PyTokenizer_GetInfo(const struct tok_state *);
8288
int _PyTokenizer_GetStatus(const struct tok_state *);
89+
int _PyTokenizer_GetNewlineRequirements(const struct tok_state *);
8390
/* An absent token span has a nonnull empty text view. */
8491
const char *_PyToken_TextView(
8592
const struct tok_state *, const struct token *, Py_ssize_t *);

0 commit comments

Comments
 (0)