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
38 changes: 38 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7475,6 +7475,12 @@ 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'
# Collect all names in scope to suggest similar alternatives
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 +7505,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 for suggestions and might introduce noise
return {n for n in names if not n.startswith("__") or n.endswith("__")}

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
Loading