diff --git a/mypy/semanal.py b/mypy/semanal.py index 20bcb2f4ac60a..283ad57f12def 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7475,6 +7475,18 @@ 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 + and ctx.line not in self.errors.ignored_lines.get(self.errors.file, {}) + ): + 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 +7511,39 @@ 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 (only when directly in class body, not in methods) + - 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.is_class_scope(): + assert self.type is not None + names.update(self.type.names.keys()) + + names.update(self.globals.keys()) + + b = self.globals.get("__builtins__", None) + if b: + assert isinstance(b.node, MypyFile) + 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: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 58f48144b3e56..e4d9f663496a5 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -21,6 +21,77 @@ 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 testErrorCodeUndefinedNameSuggestionLocal] +def foo() -> None: + local_value = 10 + x = local_valeu # E: Name "local_valeu" is not defined; did you mean "local_value"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionOuterScope] +top_level_var = 42 + +def foo() -> None: + x = top_level_vr # E: Name "top_level_vr" is not defined; did you mean "top_level_var"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionClassBody] +class Foo: + class_attr = 10 + x = class_atr # E: Name "class_atr" is not defined; did you mean "class_attr"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionFromImport] +from m import some_function + +some_functon(1) # E: Name "some_functon" is not defined; did you mean "some_function"? [name-defined] + +[file m.py] +def some_function(x: int) -> int: + return x +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionBuiltin] +x = lenn # E: Name "lenn" is not defined; did you mean "len"? [name-defined] +[builtins fixtures/len.pyi] + +[case testErrorCodeUndefinedNameSuggestionClassMethod] +class Foo: + class_attr = 10 + def method(self) -> None: + x = class_atr # E: Name "class_atr" is not defined [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionMultiple] +total_count = 1 +total_counts = 2 +x = total_countt # E: Name "total_countt" is not defined; did you mean "total_count" or "total_counts"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionIgnored] +my_variable = 42 +x = my_variabel # type: ignore[name-defined] +y = my_variabel # type: ignore +[builtins fixtures/module.pyi] + [case testErrorCodeUnclassifiedError] class A: def __init__(self) -> int: \ diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 9f057042926d3..6c91d0d2d5ce1 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -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