diff --git a/mypy/semanal.py b/mypy/semanal.py index 20bcb2f4ac60a..0c4647eaf625c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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: @@ -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: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 58f48144b3e56..eca5a2e687c80 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -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: \