@@ -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