-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Improve "Name is not defined" errors with fuzzy matching #20693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
6ca3d6f
115b831
31d0bd2
55a8efc
5825537
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
|
@@ -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()) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Class namespace should only be included if we are not within a method (i.e. directly within class body only). |
||
|
|
||
| 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: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideas for additional tests:
|
||
| [builtins fixtures/module.pyi] | ||
|
|
||
| [case testErrorCodeUnclassifiedError] | ||
| class A: | ||
| def __init__(self) -> int: \ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add an explicit test in some |
||
|
|
||
| [case testVarArgsFunctionSubtyping] | ||
| import typing | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is pretty expensive, we shouldn't do the analysis if the error will just be ignored. This way we'd only slow things down when there is a user-visible error. In some codebases, there could be thousands of ignored errors, and this could generate non-trivial amounts of extra work.
My suggestion would be to refactor
is_ignored_errorinmypy/errors.pyso that most of the checking (but not the blocker check) is available throughis_ignored_error_code(line, code, ignores), for example. Then you can check ifcodes.NAME_DEFINEDis ignored here, and skip the expensive logic.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it would be fine to skip the logic if there is any ignore comment on the line, if that's easier to implement.