Skip to content

Commit 7b56d08

Browse files
[3.14] gh-137078: Fix keyword typo recognition when executed over files (GH-137079) (#137826)
1 parent 4cb99bf commit 7b56d08

File tree

6 files changed

+56
-7
lines changed

6 files changed

+56
-7
lines changed

Grammar/python.gram

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,12 @@ func_type[mod_ty]: '(' a=[type_expressions] ')' '->' b=expression NEWLINE* ENDMA
9494
# GENERAL STATEMENTS
9595
# ==================
9696

97-
statements[asdl_stmt_seq*]: a=statement+ { _PyPegen_register_stmts(p, (asdl_stmt_seq*)_PyPegen_seq_flatten(p, a)) }
97+
statements[asdl_stmt_seq*]: a=statement+ { (asdl_stmt_seq*)_PyPegen_seq_flatten(p, a) }
9898

9999
statement[asdl_stmt_seq*]:
100-
| a=compound_stmt { (asdl_stmt_seq*)_PyPegen_singleton_seq(p, a) }
100+
| a=compound_stmt { _PyPegen_register_stmts(p ,
101+
(asdl_stmt_seq*)_PyPegen_singleton_seq(p, a)
102+
) }
101103
| a[asdl_stmt_seq*]=simple_stmts { a }
102104

103105
single_compound_stmt[asdl_stmt_seq*]:

Lib/test/test_traceback.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
requires_debug_ranges, has_no_debug_ranges,
2020
requires_subprocess)
2121
from test.support.os_helper import TESTFN, unlink
22-
from test.support.script_helper import assert_python_ok, assert_python_failure
22+
from test.support.script_helper import assert_python_ok, assert_python_failure, make_script
2323
from test.support.import_helper import forget
2424
from test.support import force_not_colorized, force_not_colorized_test_class
2525

@@ -1740,6 +1740,49 @@ def f():
17401740
]
17411741
self.assertEqual(result_lines, expected)
17421742

1743+
class TestKeywordTypoSuggestions(unittest.TestCase):
1744+
TYPO_CASES = [
1745+
("with block ad something:\n pass", "and"),
1746+
("fur a in b:\n pass", "for"),
1747+
("for a in b:\n pass\nelso:\n pass", "else"),
1748+
("whille True:\n pass", "while"),
1749+
("iff x > 5:\n pass", "if"),
1750+
("if x:\n pass\nelseif y:\n pass", "elif"),
1751+
("tyo:\n pass\nexcept y:\n pass", "try"),
1752+
("classe MyClass:\n pass", "class"),
1753+
("impor math", "import"),
1754+
("form x import y", "from"),
1755+
("defn calculate_sum(a, b):\n return a + b", "def"),
1756+
("def foo():\n returm result", "return"),
1757+
("lamda x: x ** 2", "lambda"),
1758+
("def foo():\n yeld i", "yield"),
1759+
("def foo():\n globel counter", "global"),
1760+
("frum math import sqrt", "from"),
1761+
("asynch def fetch_data():\n pass", "async"),
1762+
("async def foo():\n awaid fetch_data()", "await"),
1763+
('raisee ValueError("Error")', "raise"),
1764+
("[x for x\nin range(3)\nof x]", "if"),
1765+
("[123 fur x\nin range(3)\nif x]", "for"),
1766+
("for x im n:\n pass", "in"),
1767+
]
1768+
1769+
def test_keyword_suggestions_from_file(self):
1770+
with tempfile.TemporaryDirectory() as script_dir:
1771+
for i, (code, expected_kw) in enumerate(self.TYPO_CASES):
1772+
with self.subTest(typo=expected_kw):
1773+
source = textwrap.dedent(code).strip()
1774+
script_name = make_script(script_dir, f"script_{i}", source)
1775+
rc, stdout, stderr = assert_python_failure(script_name)
1776+
stderr_text = stderr.decode('utf-8')
1777+
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
1778+
1779+
def test_keyword_suggestions_from_command_string(self):
1780+
for code, expected_kw in self.TYPO_CASES:
1781+
with self.subTest(typo=expected_kw):
1782+
source = textwrap.dedent(code).strip()
1783+
rc, stdout, stderr = assert_python_failure('-c', source)
1784+
stderr_text = stderr.decode('utf-8')
1785+
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
17431786

17441787
@requires_debug_ranges()
17451788
@force_not_colorized_test_class

Lib/traceback.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,7 +1310,6 @@ def _find_keyword_typos(self):
13101310
lines = source.splitlines()
13111311

13121312
error_code = lines[line -1 if line > 0 else 0:end_line]
1313-
error_code[0] = error_code[0][offset:]
13141313
error_code = textwrap.dedent('\n'.join(error_code))
13151314

13161315
# Do not continue if the source is too large
@@ -1326,7 +1325,8 @@ def _find_keyword_typos(self):
13261325
if token.type != tokenize.NAME:
13271326
continue
13281327
# Only consider NAME tokens on the same line as the error
1329-
if from_filename and token.start[0]+line != end_line+1:
1328+
the_end = end_line if line == 0 else end_line + 1
1329+
if from_filename and token.start[0]+line != the_end:
13301330
continue
13311331
wrong_name = token.string
13321332
if wrong_name in keyword.kwlist:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix keyword typo recognition when parsing files. Patch by Pablo Galindo.

Parser/action_helpers.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,6 +1939,9 @@ _PyPegen_register_stmts(Parser *p, asdl_stmt_seq* stmts) {
19391939
return stmts;
19401940
}
19411941
stmt_ty last_stmt = asdl_seq_GET(stmts, len - 1);
1942+
if (p->last_stmt_location.lineno > last_stmt->lineno) {
1943+
return stmts;
1944+
}
19421945
p->last_stmt_location.lineno = last_stmt->lineno;
19431946
p->last_stmt_location.col_offset = last_stmt->col_offset;
19441947
p->last_stmt_location.end_lineno = last_stmt->end_lineno;

Parser/parser.c

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)