Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7475,6 +7475,17 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None
self.record_incomplete_ref()
return
message = f'Name "{name}" is not defined'
if (
not self.msg.prefer_simple_messages()
and "." not in name
and not (name.startswith("__") and name.endswith("__"))
and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES
):
alternatives = self._get_names_in_scope()
alternatives.discard(name)
matches = best_matches(name, alternatives, n=3)
if matches:
message += f"; did you mean {pretty_seq(matches, 'or')}?"
self.fail(message, ctx, code=codes.NAME_DEFINED)

if f"builtins.{name}" in SUGGESTED_TEST_FIXTURES:
Expand All @@ -7499,6 +7510,38 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None
).format(module=module, name=lowercased[fullname].rsplit(".", 1)[-1])
self.note(hint, ctx, code=codes.NAME_DEFINED)

def _get_names_in_scope(self) -> set[str]:
"""Collect all names visible in the current scope for fuzzy matching suggestions.

This includes:
- Local variables (from function scopes)
- Class attributes (if it's inside a class)
- Global/module-level names
- Builtins
"""
names: set[str] = set()

for table in self.locals:
if table is not None:
names.update(table.keys())

if self.type is not None:
names.update(self.type.names.keys())

names.update(self.globals.keys())

b = self.globals.get("__builtins__", None)
if b and isinstance(b.node, MypyFile):
# Only include public builtins (not _private ones)
for builtin_name in b.node.names.keys():
if not (
len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"
):
names.add(builtin_name)

# Filter out internal/dunder names that aren't useful as suggestions
return {n for n in names if not n.startswith("__")}

def already_defined(
self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None, noun: str
) -> None:
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ def f() -> None:
[file m.py]
[builtins fixtures/module.pyi]

[case testErrorCodeUndefinedNameSuggestion]
my_variable = 42
my_constant = 100

x = my_variabel # E: Name "my_variabel" is not defined; did you mean "my_variable"? [name-defined]

def calculate_sum(items: int) -> int:
return items

calculate_summ(1) # E: Name "calculate_summ" is not defined; did you mean "calculate_sum"? [name-defined]

class MyClass:
pass

y = MyClas() # E: Name "MyClas" is not defined; did you mean "MyClass"? [name-defined]

unknown_xyz # E: Name "unknown_xyz" is not defined [name-defined]
[builtins fixtures/module.pyi]

[case testErrorCodeUnclassifiedError]
class A:
def __init__(self) -> int: \
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ def f(x: _T) -> None: pass
s: FrozenSet
[out]
_program.py:2: error: Name "_T" is not defined
_program.py:3: error: Name "FrozenSet" is not defined
_program.py:3: error: Name "FrozenSet" is not defined; did you mean "frozenset"?

[case testVarArgsFunctionSubtyping]
import typing
Expand Down