@@ -1142,7 +1142,43 @@ add_keyword_typo(KeywordTypo *candidates, int *count, int mark,
11421142}
11431143
11441144static int
1145- try_keyword_typo(Parser *p, KeywordTypo *candidate, KeywordTrial *state)
1145+ keyword_trial_accepts(Parser *p, const KeywordTypo *candidate)
1146+ {
1147+ int accepted = _PyPegen_parse(p) != NULL;
1148+ if (p->error_indicator || PyErr_Occurred()) {
1149+ return 0;
1150+ }
1151+ for (int i = p->mark; accepted && i < p->fill; i++) {
1152+ int type = p->tokens[i]->type;
1153+ accepted = type == NEWLINE || type == ENDMARKER;
1154+ }
1155+ if (accepted) {
1156+ return 1;
1157+ }
1158+ // Version checks on an enclosing rule can run after its suite. For older
1159+ // grammars, require a complete repair so those checks execute.
1160+ if (p->feature_version != PY_MINOR_VERSION) {
1161+ return 0;
1162+ }
1163+ // Known foreign keywords can identify a valid header even when its body
1164+ // contains another error (for example, "switch x:\n case:"). Require the
1165+ // ordinary grammar to advance into a later line before accepting that hint.
1166+ if (candidate->is_alias) {
1167+ for (int i = candidate->mark; i < p->keyword_trial->furthest; i++) {
1168+ if (p->tokens[i]->type == NEWLINE) {
1169+ return 1;
1170+ }
1171+ }
1172+ }
1173+ reset_parser_state_for_error_pass(p);
1174+ _PyPegen_parse(p);
1175+ return p->keyword_trial->incomplete_suite;
1176+ }
1177+
1178+ // Return 1 for an accepted repair, 0 for a rejected repair, or -1 to stop the
1179+ // search. Raw allocation failures need not set an exception.
1180+ static int
1181+ try_keyword_typo(Parser *p, const KeywordTypo *candidate, KeywordTrial *state)
11461182{
11471183 // Token text belongs to the original arena. Token headers and memo entries
11481184 // must be private: even a failed trial can memoize nodes containing its edit.
@@ -1195,31 +1231,7 @@ try_keyword_typo(Parser *p, KeywordTypo *candidate, KeywordTrial *state)
11951231 trial.known_err_token = NULL;
11961232 trial.errcode = NULL;
11971233 trial.normalize = NULL;
1198- accepted = _PyPegen_parse(&trial) != NULL &&
1199- !trial.error_indicator && !PyErr_Occurred();
1200- for (int i = trial.mark; accepted && i < trial.fill; i++) {
1201- accepted = tokens[i].type == NEWLINE || tokens[i].type == ENDMARKER;
1202- }
1203- // Known foreign keywords may identify a valid header even when its body
1204- // contains another error (for example, "switch x:\n case:"). Require the
1205- // ordinary grammar to advance into a later line before accepting that hint.
1206- if (!accepted && !trial.error_indicator && candidate->is_alias &&
1207- p->feature_version == PY_MINOR_VERSION)
1208- {
1209- for (int i = candidate->mark; i < state->furthest; i++) {
1210- if (tokens[i].type == NEWLINE) {
1211- accepted = 1;
1212- break;
1213- }
1214- }
1215- }
1216- if (!accepted && !trial.error_indicator && !PyErr_Occurred()) {
1217- reset_parser_state_for_error_pass(&trial);
1218- _PyPegen_parse(&trial);
1219- // Version checks on an enclosing rule can run after its suite. For
1220- // older grammars, require a complete repair so those checks execute.
1221- accepted = state->incomplete_suite && p->feature_version == PY_MINOR_VERSION;
1222- }
1234+ accepted = keyword_trial_accepts(&trial, candidate);
12231235done:
12241236 // No Parser_Free: the trial borrows the tokenizer and type-ignore comments,
12251237 // and its token headers are a single allocation.
@@ -1233,39 +1245,60 @@ try_keyword_typo(Parser *p, KeywordTypo *candidate, KeywordTrial *state)
12331245 return PyErr_Occurred() ? -1 : accepted;
12341246}
12351247
1236- static PyObject *
1237- keyword_typo_metadata (Parser *p, PySyntaxErrorObject *error )
1248+ static const char *
1249+ keyword_typo_text (Parser *p, Token *token, int at_error, int *is_keyword )
12381250{
1239- // A real ENDMARKER is required even for an unfinished suite. A truncated
1240- // token cache is never evidence that a replacement is valid.
1241- if ((p->start_rule != Py_file_input && p->start_rule != Py_eval_input &&
1242- p->start_rule != Py_single_input) ||
1243- p->fill == 0 || p->fill > MAX_KEYWORD_TYPO_TOKENS ||
1244- _PyTokenizer_GetInfo(p->tok).input_span.end > MAX_KEYWORD_TYPO_BYTES ||
1245- p->tokens[p->fill - 1]->type != ENDMARKER ||
1246- _PyTokenizer_IsInteractive(p->tok) ||
1247- !Py_IS_TYPE(error, (PyTypeObject *)PyExc_SyntaxError))
1248- {
1251+ // A keyword can itself be a typo, as in "for x is y". Consider an existing
1252+ // keyword only at the reported error, not throughout the statement.
1253+ *is_keyword = token->type != NAME;
1254+ if (*is_keyword) {
1255+ if (!at_error) {
1256+ return NULL;
1257+ }
1258+ // Hard keywords have no token text. Use the generated keyword table.
1259+ for (int length = 0; length < p->n_keyword_lists; length++) {
1260+ for (KeywordToken *k = p->keywords[length];
1261+ k != NULL && k->type != -1; k++)
1262+ {
1263+ if (k->type == token->type) {
1264+ return k->str;
1265+ }
1266+ }
1267+ }
12491268 return NULL;
12501269 }
1270+ const char *text = PyBytes_AS_STRING(token->bytes);
1271+ for (char **k = p->soft_keywords; k != NULL && *k != NULL; k++) {
1272+ if (strcmp(text, *k) == 0) {
1273+ *is_keyword = 1;
1274+ return at_error ? text : NULL;
1275+ }
1276+ }
1277+ return text;
1278+ }
1279+
1280+ // Return the candidate count, or -1 if allocation or capacity prevents a
1281+ // complete search. Capacity exhaustion does not set an exception.
1282+ static int
1283+ collect_keyword_typos(Parser *p, PySyntaxErrorObject *error, KeywordTypo *candidates)
1284+ {
12511285 long error_line = PyLong_AsLong(error->lineno);
12521286 long error_offset = PyLong_AsLong(error->offset);
12531287 if (error_line < 0 || error_offset < 0) {
1254- return NULL ;
1288+ return -1 ;
12551289 }
12561290 Py_ssize_t error_col = -1;
12571291 if (error_offset > 0 && error->text != NULL && PyUnicode_Check(error->text)) {
12581292 PyObject *prefix = PyUnicode_Substring(error->text, 0, error_offset - 1);
12591293 if (prefix == NULL) {
1260- return NULL ;
1294+ return -1 ;
12611295 }
1262- const char *text = PyUnicode_AsUTF8AndSize(prefix, &error_col);
1296+ int failed = PyUnicode_AsUTF8AndSize(prefix, &error_col) == NULL ;
12631297 Py_DECREF(prefix);
1264- if (text == NULL ) {
1265- return NULL ;
1298+ if (failed ) {
1299+ return -1 ;
12661300 }
12671301 }
1268- KeywordTypo candidates[MAX_KEYWORD_TYPO_CANDIDATES];
12691302 int count = 0;
12701303 int start = 0;
12711304 for (int i = 0; i < p->fill && p->tokens[i]->lineno < error_line; i++) {
@@ -1287,34 +1320,15 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
12871320 if (token->type == NEWLINE) {
12881321 break;
12891322 }
1290- const char *text = token->type == NAME ? PyBytes_AS_STRING(token->bytes) : NULL;
1291- Py_ssize_t text_length = text == NULL ? 0 : PyBytes_GET_SIZE(token->bytes);
1292- int is_keyword = token->type != NAME;
1293- // A keyword can itself be a typo, as in "for x is y". Consider an
1294- // existing keyword only at the reported error, not throughout the line.
1295- if (text == NULL && token->lineno == error_line && token->col_offset == error_col) {
1296- for (int length = 0; length < p->n_keyword_lists && text == NULL; length++) {
1297- for (KeywordToken *k = p->keywords[length];
1298- k != NULL && k->type != -1; k++)
1299- {
1300- if (k->type == token->type) {
1301- text = k->str;
1302- text_length = length;
1303- break;
1304- }
1305- }
1306- }
1307- }
1308- if (text == NULL || text_length < 2 || text_length > 10) {
1323+ int at_error = token->lineno == error_line && token->col_offset == error_col;
1324+ int is_keyword;
1325+ const char *text = keyword_typo_text(p, token, at_error, &is_keyword);
1326+ if (text == NULL) {
13091327 continue;
13101328 }
1311- for (char **k = p->soft_keywords; k != NULL && *k != NULL; k++) {
1312- if (strcmp(text, *k) == 0) {
1313- is_keyword = 1;
1314- break;
1315- }
1316- }
1317- if (is_keyword && (token->lineno != error_line || token->col_offset != error_col)) {
1329+ Py_ssize_t text_length = token->type == NAME ?
1330+ PyBytes_GET_SIZE(token->bytes) : (Py_ssize_t)strlen(text);
1331+ if (text_length < 2 || text_length > 10) {
13181332 continue;
13191333 }
13201334 const char *alias = NULL;
@@ -1326,14 +1340,14 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
13261340 }
13271341 if (alias != NULL) {
13281342 if (count == MAX_KEYWORD_TYPO_CANDIDATES) {
1329- return NULL ;
1343+ return -1 ;
13301344 }
13311345 candidates[count++] = (KeywordTypo){i, alias, 0, 1};
13321346 continue;
13331347 }
13341348 PyObject *name = PyUnicode_DecodeUTF8(text, text_length, NULL);
13351349 if (name == NULL) {
1336- return NULL ;
1350+ return -1 ;
13371351 }
13381352 // Preserve missing-comma diagnostics and correctly spelled keywords
13391353 // unless one edit repairs the input. For keywords, only substitute a
@@ -1358,9 +1372,61 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
13581372 }
13591373 Py_DECREF(name);
13601374 if (result < 0) {
1361- return NULL ;
1375+ return -1 ;
13621376 }
13631377 }
1378+ return count;
1379+ }
1380+
1381+ static PyObject *
1382+ build_keyword_typo_metadata(Parser *p, const KeywordTypo *candidate)
1383+ {
1384+ Token *token = p->tokens[candidate->mark];
1385+ PyObject *line;
1386+ if (_PyTokenizer_RetainedSource(p->tok) != NULL) {
1387+ Py_ssize_t size;
1388+ const char *text = _PyTokenizer_LineView(p->tok, token->lineno, &size);
1389+ line = PyUnicode_DecodeUTF8(text, size, "replace");
1390+ }
1391+ else {
1392+ _PyTokenizer_Info info = _PyTokenizer_GetInfo(p->tok);
1393+ line = _PyErr_ProgramDecodedTextObject(
1394+ info.filename, token->lineno, info.encoding);
1395+ }
1396+ if (line == NULL) {
1397+ return NULL;
1398+ }
1399+ Py_ssize_t start = _PyPegen_byte_offset_to_character_offset(line, token->col_offset);
1400+ Py_ssize_t end = _PyPegen_byte_offset_to_character_offset(line, token->end_col_offset);
1401+ PyObject *metadata = NULL;
1402+ if (start >= 0 && end >= 0) {
1403+ metadata = Py_BuildValue("(sOinin)", candidate->keyword, line,
1404+ token->lineno, start + 1,
1405+ token->end_lineno, end + 1);
1406+ }
1407+ Py_DECREF(line);
1408+ return metadata;
1409+ }
1410+
1411+ static PyObject *
1412+ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
1413+ {
1414+ // A real ENDMARKER is required even for an unfinished suite. A truncated
1415+ // token cache is never evidence that a replacement is valid.
1416+ if ((p->start_rule != Py_file_input && p->start_rule != Py_eval_input &&
1417+ p->start_rule != Py_single_input) ||
1418+ p->fill == 0 || p->fill > MAX_KEYWORD_TYPO_TOKENS ||
1419+ _PyTokenizer_GetInfo(p->tok).input_span.end > MAX_KEYWORD_TYPO_BYTES ||
1420+ p->tokens[p->fill - 1]->type != ENDMARKER ||
1421+ _PyTokenizer_IsInteractive(p->tok))
1422+ {
1423+ return NULL;
1424+ }
1425+ KeywordTypo candidates[MAX_KEYWORD_TYPO_CANDIDATES];
1426+ int count = collect_keyword_typos(p, error, candidates);
1427+ if (count <= 0) {
1428+ return NULL;
1429+ }
13641430 qsort(candidates, count, sizeof(*candidates), compare_keyword_typos);
13651431 // All candidates and both parser passes share this work budget.
13661432 KeywordTrial state = {.budget = MAX_KEYWORD_TYPO_STEPS};
@@ -1369,55 +1435,27 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
13691435 if (accepted < 0) {
13701436 return NULL;
13711437 }
1372- if (!accepted) {
1373- continue;
1374- }
1375- Token *token = p->tokens[candidates[i].mark];
1376- PyObject *line;
1377- if (_PyTokenizer_RetainedSource(p->tok) != NULL) {
1378- Py_ssize_t size;
1379- const char *text = _PyTokenizer_LineView(p->tok, token->lineno, &size);
1380- line = PyUnicode_DecodeUTF8(text, size, "replace");
1381- }
1382- else {
1383- _PyTokenizer_Info info = _PyTokenizer_GetInfo(p->tok);
1384- line = _PyErr_ProgramDecodedTextObject(
1385- info.filename, token->lineno, info.encoding);
1438+ if (accepted) {
1439+ return build_keyword_typo_metadata(p, &candidates[i]);
13861440 }
1387- if (line == NULL) {
1388- return NULL;
1389- }
1390- Py_ssize_t start = _PyPegen_byte_offset_to_character_offset(line, token->col_offset);
1391- Py_ssize_t end = _PyPegen_byte_offset_to_character_offset(line, token->end_col_offset);
1392- PyObject *metadata = NULL;
1393- if (start >= 0 && end >= 0) {
1394- metadata = Py_BuildValue("(sOinin)", candidates[i].keyword, line,
1395- token->lineno, start + 1,
1396- token->end_lineno, end + 1);
1397- }
1398- Py_DECREF(line);
1399- return metadata;
14001441 }
14011442 return NULL;
14021443}
14031444
14041445static void
1405- _PyPegen_set_syntax_error_metadata(Parser *p) {
1446+ _PyPegen_set_syntax_error_metadata(Parser *p)
1447+ {
14061448 PyObject *exc = PyErr_GetRaisedException();
1407- if (!exc || !PyObject_TypeCheck(exc, (PyTypeObject *)PyExc_SyntaxError)) {
1408- PyErr_SetRaisedException(exc);
1409- return;
1410- }
1411- PySyntaxErrorObject *syntax_error = (PySyntaxErrorObject *)exc;
1412- PyObject *hint = keyword_typo_metadata(p, syntax_error);
1413- if (hint != NULL) {
1414- Py_XSETREF(syntax_error->metadata, hint);
1415- PyErr_SetRaisedException(exc);
1416- return;
1417- }
1418- // Suggestions must never replace the original exception, including when
1419- // a trial runs out of memory or exhausts its rule budget.
1420- PyErr_Clear();
1449+ if (exc != NULL && Py_IS_TYPE(exc, (PyTypeObject *)PyExc_SyntaxError)) {
1450+ PySyntaxErrorObject *syntax_error = (PySyntaxErrorObject *)exc;
1451+ PyObject *hint = keyword_typo_metadata(p, syntax_error);
1452+ if (hint != NULL) {
1453+ Py_XSETREF(syntax_error->metadata, hint);
1454+ }
1455+ // Suggestions must never replace the original exception, including
1456+ // when a trial runs out of memory or exhausts its rule budget.
1457+ PyErr_Clear();
1458+ }
14211459 PyErr_SetRaisedException(exc);
14221460}
14231461
0 commit comments