@@ -1022,6 +1022,7 @@ _PyPegen_Parser_New(struct tok_state *tok, int start_rule, int flags,
10221022#endif
10231023 p -> level = 0 ;
10241024 p -> call_invalid_rules = 0 ;
1025+ p -> missing_comma = 0 ;
10251026 p -> input_exhausted = 0 ;
10261027 p -> keyword_trial = NULL ;
10271028#ifdef Py_DEBUG
@@ -1107,6 +1108,22 @@ add_keyword_typo(KeywordTypo *candidates, int *count, int mark,
11071108 max_cost = Py_MIN (max_cost , 2 );
11081109 }
11091110 Py_ssize_t cost = _Py_UTF8_Edit_Cost (name , word , max_cost );
1111+ // Adjacent transpositions are one spelling error. The shared Levenshtein
1112+ // distance counts them as two substitutions.
1113+ if (cost > 2 && PyUnicode_IS_ASCII (name ) &&
1114+ PyUnicode_GET_LENGTH (name ) == PyUnicode_GET_LENGTH (word ))
1115+ {
1116+ const char * text = (const char * )PyUnicode_1BYTE_DATA (name );
1117+ Py_ssize_t i = 0 ;
1118+ while (text [i ] && text [i ] == keyword [i ]) {
1119+ i ++ ;
1120+ }
1121+ if (text [i ] && text [i + 1 ] && text [i ] == keyword [i + 1 ] &&
1122+ text [i + 1 ] == keyword [i ] && strcmp (text + i + 2 , keyword + i + 2 ) == 0 )
1123+ {
1124+ cost = 2 ;
1125+ }
1126+ }
11101127 Py_DECREF (word );
11111128 if (cost < 0 ) {
11121129 return -1 ;
@@ -1227,23 +1244,28 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
12271244 p -> fill == 0 || p -> fill > MAX_KEYWORD_TYPO_TOKENS ||
12281245 _PyTokenizer_GetInfo (p -> tok ).input_span .end > MAX_KEYWORD_TYPO_BYTES ||
12291246 p -> tokens [p -> fill - 1 ]-> type != ENDMARKER ||
1230- _PyTokenizer_IsInteractive (p -> tok ) || !PyUnicode_Check (error -> msg ))
1247+ _PyTokenizer_IsInteractive (p -> tok ) ||
1248+ !Py_IS_TYPE (error , (PyTypeObject * )PyExc_SyntaxError ))
12311249 {
12321250 return NULL ;
12331251 }
1234- const char * message = PyUnicode_AsUTF8 (error -> msg );
1235- if (message == NULL || (strcmp (message , "invalid syntax" ) != 0 &&
1236- strstr (message , "Perhaps you forgot a comma" ) == NULL ))
1237- {
1238- return NULL ;
1239- }
1240- // Prefer an existing missing-comma diagnostic unless the spelling differs
1241- // by only one edit (the shared edit-cost function charges two per edit).
1242- Py_ssize_t max_cost = strcmp (message , "invalid syntax" ) == 0 ? 4 : 2 ;
12431252 long error_line = PyLong_AsLong (error -> lineno );
1244- if (error_line < 0 ) {
1253+ long error_offset = PyLong_AsLong (error -> offset );
1254+ if (error_line < 0 || error_offset < 0 ) {
12451255 return NULL ;
12461256 }
1257+ Py_ssize_t error_col = -1 ;
1258+ if (error_offset > 0 && error -> text != NULL && PyUnicode_Check (error -> text )) {
1259+ PyObject * prefix = PyUnicode_Substring (error -> text , 0 , error_offset - 1 );
1260+ if (prefix == NULL ) {
1261+ return NULL ;
1262+ }
1263+ const char * text = PyUnicode_AsUTF8AndSize (prefix , & error_col );
1264+ Py_DECREF (prefix );
1265+ if (text == NULL ) {
1266+ return NULL ;
1267+ }
1268+ }
12471269 KeywordTypo candidates [MAX_KEYWORD_TYPO_CANDIDATES ];
12481270 int count = 0 ;
12491271 int start = 0 ;
@@ -1261,15 +1283,44 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
12611283 };
12621284 for (int i = start ; i < p -> fill ; i ++ ) {
12631285 Token * token = p -> tokens [i ];
1264- if (token -> type != NAME || token -> lineno > error_line ||
1265- PyBytes_GET_SIZE (token -> bytes ) < 2 ||
1266- PyBytes_GET_SIZE (token -> bytes ) > 10 )
1267- {
1286+ // Error spans can start before the typo in a multiline expression.
1287+ // Keep the complete logical statement as the candidate range.
1288+ if (token -> type == NEWLINE ) {
1289+ break ;
1290+ }
1291+ const char * text = token -> type == NAME ? PyBytes_AS_STRING (token -> bytes ) : NULL ;
1292+ Py_ssize_t text_length = text == NULL ? 0 : PyBytes_GET_SIZE (token -> bytes );
1293+ int is_keyword = token -> type != NAME ;
1294+ // A keyword can itself be a typo, as in "for x is y". Consider an
1295+ // existing keyword only at the reported error, not throughout the line.
1296+ if (text == NULL && token -> lineno == error_line && token -> col_offset == error_col ) {
1297+ for (int length = 0 ; length < p -> n_keyword_lists && text == NULL ; length ++ ) {
1298+ for (KeywordToken * k = p -> keywords [length ];
1299+ k != NULL && k -> type != -1 ; k ++ )
1300+ {
1301+ if (k -> type == token -> type ) {
1302+ text = k -> str ;
1303+ text_length = length ;
1304+ break ;
1305+ }
1306+ }
1307+ }
1308+ }
1309+ if (text == NULL || text_length < 2 || text_length > 10 ) {
1310+ continue ;
1311+ }
1312+ for (char * * k = p -> soft_keywords ; k != NULL && * k != NULL ; k ++ ) {
1313+ if (strcmp (text , * k ) == 0 ) {
1314+ is_keyword = 1 ;
1315+ break ;
1316+ }
1317+ }
1318+ if (is_keyword && (token -> lineno != error_line || token -> col_offset != error_col )) {
12681319 continue ;
12691320 }
12701321 const char * alias = NULL ;
12711322 for (size_t j = 0 ; j < Py_ARRAY_LENGTH (aliases ); j ++ ) {
1272- if (strcmp (PyBytes_AS_STRING ( token -> bytes ) , aliases [j ].name ) == 0 ) {
1323+ if (strcmp (text , aliases [j ].name ) == 0 ) {
12731324 alias = aliases [j ].keyword ;
12741325 break ;
12751326 }
@@ -1281,20 +1332,29 @@ keyword_typo_metadata(Parser *p, PySyntaxErrorObject *error)
12811332 candidates [count ++ ] = (KeywordTypo ){i , alias , 0 , 1 };
12821333 continue ;
12831334 }
1284- PyObject * name = PyUnicode_DecodeUTF8 (
1285- PyBytes_AS_STRING (token -> bytes ), PyBytes_GET_SIZE (token -> bytes ), NULL );
1335+ PyObject * name = PyUnicode_DecodeUTF8 (text , text_length , NULL );
12861336 if (name == NULL ) {
12871337 return NULL ;
12881338 }
1339+ // Preserve missing-comma diagnostics and correctly spelled keywords
1340+ // unless one edit repairs the input. For keywords, only substitute a
1341+ // character: deleting one from "for" must not suggest "or".
1342+ Py_ssize_t max_cost = p -> missing_comma || is_keyword ? 2 : 4 ;
12891343 int result = 0 ;
12901344 for (int length = 0 ; length < p -> n_keyword_lists && result == 0 ; length ++ ) {
1345+ if (is_keyword && length != text_length ) {
1346+ continue ;
1347+ }
12911348 for (KeywordToken * k = p -> keywords [length ];
12921349 k != NULL && k -> type != -1 && result == 0 ; k ++ )
12931350 {
12941351 result = add_keyword_typo (candidates , & count , i , name , k -> str , max_cost );
12951352 }
12961353 }
12971354 for (char * * k = p -> soft_keywords ; k != NULL && * k != NULL && result == 0 ; k ++ ) {
1355+ if (is_keyword && (Py_ssize_t )strlen (* k ) != text_length ) {
1356+ continue ;
1357+ }
12981358 result = add_keyword_typo (candidates , & count , i , name , * k , max_cost );
12991359 }
13001360 Py_DECREF (name );
0 commit comments