Skip to content

Commit 620af70

Browse files
authored
Improve "Name is not defined" errors with fuzzy matching (#20693)
This PR collects all visible names in the current scope and uses fuzzy matching to provide suggestions in the error messages
1 parent b7e311e commit 620af70

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

mypy/semanal.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7529,6 +7529,18 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None
75297529
self.record_incomplete_ref()
75307530
return
75317531
message = f'Name "{name}" is not defined'
7532+
if (
7533+
not self.msg.prefer_simple_messages()
7534+
and "." not in name
7535+
and not (name.startswith("__") and name.endswith("__"))
7536+
and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES
7537+
and ctx.line not in self.errors.ignored_lines.get(self.errors.file, {})
7538+
):
7539+
alternatives = self._get_names_in_scope()
7540+
alternatives.discard(name)
7541+
matches = best_matches(name, alternatives, n=3)
7542+
if matches:
7543+
message += f"; did you mean {pretty_seq(matches, 'or')}?"
75327544
self.fail(message, ctx, code=codes.NAME_DEFINED)
75337545

75347546
if f"builtins.{name}" in SUGGESTED_TEST_FIXTURES:
@@ -7553,6 +7565,39 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None
75537565
).format(module=module, name=lowercased[fullname].rsplit(".", 1)[-1])
75547566
self.note(hint, ctx, code=codes.NAME_DEFINED)
75557567

7568+
def _get_names_in_scope(self) -> set[str]:
7569+
"""Collect all names visible in the current scope for fuzzy matching suggestions.
7570+
7571+
This includes:
7572+
- Local variables (from function scopes)
7573+
- Class attributes (only when directly in class body, not in methods)
7574+
- Global/module-level names
7575+
- Builtins
7576+
"""
7577+
names: set[str] = set()
7578+
7579+
for table in self.locals:
7580+
if table is not None:
7581+
names.update(table.keys())
7582+
7583+
if self.is_class_scope():
7584+
assert self.type is not None
7585+
names.update(self.type.names.keys())
7586+
7587+
names.update(self.globals.keys())
7588+
7589+
b = self.globals.get("__builtins__", None)
7590+
if b:
7591+
assert isinstance(b.node, MypyFile)
7592+
for builtin_name in b.node.names.keys():
7593+
if not (
7594+
len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"
7595+
):
7596+
names.add(builtin_name)
7597+
7598+
# Filter out internal/dunder names that aren't useful as suggestions
7599+
return {n for n in names if not n.startswith("__")}
7600+
75567601
def already_defined(
75577602
self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None, noun: str
75587603
) -> None:

test-data/unit/check-errorcodes.test

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,77 @@ def f() -> None:
2121
[file m.py]
2222
[builtins fixtures/module.pyi]
2323

24+
[case testErrorCodeUndefinedNameSuggestion]
25+
my_variable = 42
26+
my_constant = 100
27+
28+
x = my_variabel # E: Name "my_variabel" is not defined; did you mean "my_variable"? [name-defined]
29+
30+
def calculate_sum(items: int) -> int:
31+
return items
32+
33+
calculate_summ(1) # E: Name "calculate_summ" is not defined; did you mean "calculate_sum"? [name-defined]
34+
35+
class MyClass:
36+
pass
37+
38+
y = MyClas() # E: Name "MyClas" is not defined; did you mean "MyClass"? [name-defined]
39+
40+
unknown_xyz # E: Name "unknown_xyz" is not defined [name-defined]
41+
[builtins fixtures/module.pyi]
42+
43+
[case testErrorCodeUndefinedNameSuggestionLocal]
44+
def foo() -> None:
45+
local_value = 10
46+
x = local_valeu # E: Name "local_valeu" is not defined; did you mean "local_value"? [name-defined]
47+
[builtins fixtures/module.pyi]
48+
49+
[case testErrorCodeUndefinedNameSuggestionOuterScope]
50+
top_level_var = 42
51+
52+
def foo() -> None:
53+
x = top_level_vr # E: Name "top_level_vr" is not defined; did you mean "top_level_var"? [name-defined]
54+
[builtins fixtures/module.pyi]
55+
56+
[case testErrorCodeUndefinedNameSuggestionClassBody]
57+
class Foo:
58+
class_attr = 10
59+
x = class_atr # E: Name "class_atr" is not defined; did you mean "class_attr"? [name-defined]
60+
[builtins fixtures/module.pyi]
61+
62+
[case testErrorCodeUndefinedNameSuggestionFromImport]
63+
from m import some_function
64+
65+
some_functon(1) # E: Name "some_functon" is not defined; did you mean "some_function"? [name-defined]
66+
67+
[file m.py]
68+
def some_function(x: int) -> int:
69+
return x
70+
[builtins fixtures/module.pyi]
71+
72+
[case testErrorCodeUndefinedNameSuggestionBuiltin]
73+
x = lenn # E: Name "lenn" is not defined; did you mean "len"? [name-defined]
74+
[builtins fixtures/len.pyi]
75+
76+
[case testErrorCodeUndefinedNameSuggestionClassMethod]
77+
class Foo:
78+
class_attr = 10
79+
def method(self) -> None:
80+
x = class_atr # E: Name "class_atr" is not defined [name-defined]
81+
[builtins fixtures/module.pyi]
82+
83+
[case testErrorCodeUndefinedNameSuggestionMultiple]
84+
total_count = 1
85+
total_counts = 2
86+
x = total_countt # E: Name "total_countt" is not defined; did you mean "total_count" or "total_counts"? [name-defined]
87+
[builtins fixtures/module.pyi]
88+
89+
[case testErrorCodeUndefinedNameSuggestionIgnored]
90+
my_variable = 42
91+
x = my_variabel # type: ignore[name-defined]
92+
y = my_variabel # type: ignore
93+
[builtins fixtures/module.pyi]
94+
2495
[case testErrorCodeUnclassifiedError]
2596
class A:
2697
def __init__(self) -> int: \

test-data/unit/pythoneval.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ def f(x: _T) -> None: pass
608608
s: FrozenSet
609609
[out]
610610
_program.py:2: error: Name "_T" is not defined
611-
_program.py:3: error: Name "FrozenSet" is not defined
611+
_program.py:3: error: Name "FrozenSet" is not defined; did you mean "frozenset"?
612612

613613
[case testVarArgsFunctionSubtyping]
614614
import typing

0 commit comments

Comments
 (0)